Compare commits
8 Commits
fix/title
...
feat/open-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7238171fa | ||
|
|
b5b34504c6 | ||
|
|
1e044405a4 | ||
|
|
72d697f6d7 | ||
|
|
57d86542e0 | ||
|
|
36e8e6051b | ||
|
|
4f9917bc5c | ||
|
|
d1b27854ea |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
72
.github/workflows/rsync-ssr.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Deploy to EC2
|
||||
on:
|
||||
workflow_dispatch: # allow manual run
|
||||
push:
|
||||
branches:
|
||||
- feat/ssr
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: pnpm/action-setup@v3.0.0
|
||||
with:
|
||||
version: 8.15.6
|
||||
|
||||
# --------------------
|
||||
# Setup configuration
|
||||
# --------------------
|
||||
- name: Prepare configuration files
|
||||
run: |
|
||||
git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/infra-config.git configuration --depth 1
|
||||
- name: Copy configuration files
|
||||
run: |
|
||||
cp configuration/dist/github/developer-roadmap.env .env
|
||||
|
||||
# --------------------
|
||||
# Prepare the build
|
||||
# --------------------
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
- name: Generate build
|
||||
run: |
|
||||
git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1
|
||||
npm run generate-renderer
|
||||
npm run build
|
||||
|
||||
# --------------------
|
||||
# Deploy to EC2
|
||||
# --------------------
|
||||
- uses: webfactory/ssh-agent@v0.7.0
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.EC2_PRIVATE_KEY }}
|
||||
- name: Deploy app to EC2
|
||||
run: |
|
||||
rsync -avz --omit-dir-times --exclude ".git" --exclude "configuration" -e "ssh -o StrictHostKeyChecking=no" -p ./ ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/var/www/v2.roadmap.sh/
|
||||
- name: Restart PM2
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.EC2_HOST }}
|
||||
username: ${{ secrets.EC2_USERNAME }}
|
||||
key: ${{ secrets.EC2_PRIVATE_KEY }}
|
||||
script: |
|
||||
cd /var/www/v2.roadmap.sh
|
||||
sudo pm2 restart web-roadmap
|
||||
|
||||
# --------------------
|
||||
# Clear Cloudfront Caching
|
||||
# --------------------
|
||||
- name: Clear Cloudfront Caching
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GH_PAT }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/roadmapsh/infra-ansible/actions/workflows/playbook.yml/dispatches \
|
||||
-d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront", "is_verbose": false } }'
|
||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
||||
'mailto:',
|
||||
'https://github.com/kamranahmedse',
|
||||
'https://thenewstack.io',
|
||||
'https://kamranahmed.info',
|
||||
'https://cs.fyi',
|
||||
'https://roadmap.sh',
|
||||
];
|
||||
if (whiteListedStarts.some((start) => href.startsWith(start))) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"image-size": "^1.1.1",
|
||||
"jose": "^5.2.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.358.0",
|
||||
"lucide-react": "^0.334.0",
|
||||
"nanoid": "^5.0.5",
|
||||
"nanostores": "^0.9.5",
|
||||
"node-html-parser": "^6.1.12",
|
||||
|
||||
8
pnpm-lock.yaml
generated
@@ -60,8 +60,8 @@ dependencies:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
lucide-react:
|
||||
specifier: ^0.358.0
|
||||
version: 0.358.0(react@18.2.0)
|
||||
specifier: ^0.334.0
|
||||
version: 0.334.0(react@18.2.0)
|
||||
nanoid:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
@@ -4236,8 +4236,8 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/lucide-react@0.358.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-rBSptRjZTMBm24zsFhR6pK/NgbT18JegZGKcH4+1H3+UigMSRpeoWLtR/fAwMYwYnlJOZB+y8WpeHne9D6X6Kg==}
|
||||
/lucide-react@0.334.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 293 KiB |
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 881 KiB |
@@ -30,8 +30,6 @@ Roadmaps are now interactive, you can click the nodes to read more about the top
|
||||
|
||||
Here is the list of available roadmaps with more being actively worked upon.
|
||||
|
||||
> Have a look at the [get started](https://roadmap.sh/get-started) page that might help you pick up a path.
|
||||
|
||||
- [Frontend Roadmap](https://roadmap.sh/frontend) / [Frontend Beginner Roadmap](https://roadmap.sh/frontend?r=frontend-beginner)
|
||||
- [Backend Roadmap](https://roadmap.sh/backend) / [Backend Beginner Roadmap](https://roadmap.sh/backend?r=backend-beginner)
|
||||
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
|
||||
@@ -39,7 +37,6 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
|
||||
- [Data Structures and Algorithms Roadmap](https://roadmap.sh/datastructures-and-algorithms)
|
||||
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
|
||||
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)
|
||||
- [MLOps Roadmap](https://roadmap.sh/mlops)
|
||||
- [QA Roadmap](https://roadmap.sh/qa)
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
@@ -77,16 +74,14 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
|
||||
There are also interactive best practices:
|
||||
|
||||
- [Backend Performance Best Practices](https://roadmap.sh/best-practices/backend-performance)
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
|
||||
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
|
||||
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
|
||||
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
|
||||
|
||||
..and questions to help you test, rate and improve your knowledge
|
||||
|
||||
- [JavaScript Questions](https://roadmap.sh/questions/javascript)
|
||||
- [Node.js Questions](https://roadmap.sh/questions/nodejs)
|
||||
- [React Questions](https://roadmap.sh/questions/react)
|
||||
|
||||

|
||||
|
||||
@@ -4,7 +4,11 @@ const path = require('path');
|
||||
const OPEN_AI_API_KEY = process.env.OPEN_AI_API_KEY;
|
||||
const ALL_BEST_PRACTICES_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices',
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const BEST_PRACTICE_JSON_DIR = path.join(
|
||||
__dirname,
|
||||
'../public/jsons/best-practices'
|
||||
);
|
||||
|
||||
const bestPracticeId = process.argv[2];
|
||||
@@ -25,14 +29,15 @@ if (!allowedBestPracticeIds.includes(bestPracticeId)) {
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
ALL_BEST_PRACTICES_DIR,
|
||||
bestPracticeId,
|
||||
'content',
|
||||
'content'
|
||||
);
|
||||
const OpenAI = require('openai');
|
||||
|
||||
const openai = new OpenAI({
|
||||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const configuration = new Configuration({
|
||||
apiKey: OPEN_AI_API_KEY,
|
||||
});
|
||||
|
||||
const openai = new OpenAIApi(configuration);
|
||||
|
||||
function getFilesInFolder(folderPath, fileList = {}) {
|
||||
const files = fs.readdirSync(folderPath);
|
||||
|
||||
@@ -57,19 +62,13 @@ function getFilesInFolder(folderPath, fileList = {}) {
|
||||
}
|
||||
|
||||
function writeTopicContent(topicTitle) {
|
||||
let prompt = `I will give you a topic and you need to write a brief paragraph with examples (if possible) about why it is important for the "${bestPracticeTitle}". Just reply to the question without adding any other information about the prompt and use simple language. Also do not start your sentences with "XYZ is important because..". Your format should be as follows:
|
||||
|
||||
# (Put a heading for the topic)
|
||||
|
||||
(Write a brief paragraph about why it is important for the "${bestPracticeTitle})
|
||||
|
||||
First topic is: ${topicTitle}`;
|
||||
let prompt = `I am reading a guide that has best practices about "${bestPracticeTitle}". I want to know more about "${topicTitle}". Write me a brief introductory paragraph about this and some tips on how I make sure of this? Behave as if you are the author of the guide.`;
|
||||
|
||||
console.log(`Generating '${topicTitle}'...`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
openai.chat.completions
|
||||
.create({
|
||||
openai
|
||||
.createChatCompletion({
|
||||
model: 'gpt-4',
|
||||
messages: [
|
||||
{
|
||||
@@ -79,7 +78,7 @@ First topic is: ${topicTitle}`;
|
||||
],
|
||||
})
|
||||
.then((response) => {
|
||||
const article = response.choices[0].message.content;
|
||||
const article = response.data.choices[0].message.content;
|
||||
|
||||
resolve(article);
|
||||
})
|
||||
@@ -91,12 +90,9 @@ First topic is: ${topicTitle}`;
|
||||
|
||||
async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
const topicId = group?.properties?.controlName;
|
||||
const topicTitle = group?.children?.controls?.control
|
||||
?.filter((control) => control?.typeID === 'Label')
|
||||
.map((control) => control?.properties?.text)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
const topicTitle = group?.children?.controls?.control?.find(
|
||||
(control) => control?.typeID === 'Label'
|
||||
)?.properties?.text;
|
||||
const currTopicUrl = `/${topicId}`;
|
||||
if (currTopicUrl.startsWith('/check:')) {
|
||||
return;
|
||||
@@ -106,6 +102,7 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
|
||||
if (!contentFilePath) {
|
||||
console.log(`Missing file for: ${currTopicUrl}`);
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,13 +123,8 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!topicTitle) {
|
||||
console.log(`Skipping ${topicId}. No title.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const topicContent = await writeTopicContent(topicTitle);
|
||||
newFileContent = `${topicContent}`;
|
||||
newFileContent += `\n\n${topicContent}`;
|
||||
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
@@ -146,14 +138,14 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
async function run() {
|
||||
const topicUrlToPathMapping = getFilesInFolder(BEST_PRACTICE_CONTENT_DIR);
|
||||
|
||||
const bestPracticeJson = require(
|
||||
path.join(ALL_BEST_PRACTICES_DIR, `${bestPracticeId}/${bestPracticeId}`),
|
||||
);
|
||||
|
||||
const bestPracticeJson = require(path.join(
|
||||
BEST_PRACTICE_JSON_DIR,
|
||||
`${bestPracticeId}.json`
|
||||
));
|
||||
const groups = bestPracticeJson?.mockup?.controls?.control?.filter(
|
||||
(control) =>
|
||||
control.typeID === '__group__' &&
|
||||
!control.properties?.controlName?.startsWith('ext_link'),
|
||||
!control.properties?.controlName?.startsWith('ext_link')
|
||||
);
|
||||
|
||||
if (!OPEN_AI_API_KEY) {
|
||||
|
||||
@@ -5,7 +5,7 @@ const CONTENT_DIR = path.join(__dirname, '../content');
|
||||
// Directory containing the best-practices
|
||||
const BEST_PRACTICE_CONTENT_DIR = path.join(
|
||||
__dirname,
|
||||
'../src/data/best-practices',
|
||||
'../src/data/best-practices'
|
||||
);
|
||||
const bestPracticeId = process.argv[2];
|
||||
|
||||
@@ -33,18 +33,18 @@ if (!bestPracticeDirName) {
|
||||
|
||||
const bestPracticeDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
bestPracticeDirName
|
||||
);
|
||||
const bestPracticeContentDirPath = path.join(
|
||||
BEST_PRACTICE_CONTENT_DIR,
|
||||
bestPracticeDirName,
|
||||
'content',
|
||||
'content'
|
||||
);
|
||||
|
||||
// If best practice content already exists do not proceed as it would override the files
|
||||
if (fs.existsSync(bestPracticeContentDirPath)) {
|
||||
console.error(
|
||||
`Best Practice content already exists @ ${bestPracticeContentDirPath}`,
|
||||
`Best Practice content already exists @ ${bestPracticeContentDirPath}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -88,12 +88,10 @@ function prepareDirTree(control, dirTree) {
|
||||
return { dirTree };
|
||||
}
|
||||
|
||||
const bestPractice = require(
|
||||
path.join(
|
||||
__dirname,
|
||||
`../src/data/best-practices/${bestPracticeId}/${bestPracticeId}`,
|
||||
),
|
||||
);
|
||||
const bestPractice = require(path.join(
|
||||
__dirname,
|
||||
`../public/jsons/best-practices/${bestPracticeId}`
|
||||
));
|
||||
const controls = bestPractice.mockup.controls.control;
|
||||
|
||||
// Prepare the dir tree that we will be creating
|
||||
|
||||
@@ -48,11 +48,6 @@ function getFilesInFolder(folderPath, fileList = {}) {
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the topic content for the given topic
|
||||
* @param currTopicUrl
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function writeTopicContent(currTopicUrl) {
|
||||
const [parentTopic, childTopic] = currTopicUrl
|
||||
.replace(/^\d+-/g, '/')
|
||||
@@ -64,18 +59,9 @@ function writeTopicContent(currTopicUrl) {
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I will give you a topic and you need to write a brief introduction for that with regards to "${roadmapTitle}". Your format should be as follows and be in strictly markdown format:
|
||||
|
||||
# (Put a heading for the topic)
|
||||
|
||||
(Write me a brief introduction for the topic with regards to "${roadmapTitle}")
|
||||
|
||||
`;
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
|
||||
if (!childTopic) {
|
||||
prompt += `First topic is: ${parentTopic}`;
|
||||
} else {
|
||||
prompt += `First topic is: ${childTopic} under ${parentTopic}`;
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
|
||||
}
|
||||
|
||||
console.log(`Generating '${childTopic || parentTopic}'...`);
|
||||
@@ -137,9 +123,10 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
|
||||
}
|
||||
|
||||
const topicContent = await writeTopicContent(currTopicUrl);
|
||||
newFileContent += `\n\n${topicContent}`;
|
||||
|
||||
console.log(`Writing ${topicId}..`);
|
||||
fs.writeFileSync(contentFilePath, topicContent, 'utf8');
|
||||
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
|
||||
|
||||
// console.log(currentFileContent);
|
||||
// console.log(currTopicUrl);
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ActivityPage() {
|
||||
|
||||
async function loadActivity() {
|
||||
const { error, response } = await httpGet<ActivityResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`,
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`
|
||||
);
|
||||
|
||||
if (!response || error) {
|
||||
@@ -79,25 +79,6 @@ export function ActivityPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const learningRoadmapsToShow = learningRoadmaps
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.filter((roadmap) => roadmap.learning > 0 || roadmap.done > 0);
|
||||
|
||||
const learningBestPracticesToShow = learningBestPractices
|
||||
.sort((a, b) => {
|
||||
const updatedAtA = new Date(a.updatedAt);
|
||||
const updatedAtB = new Date(b.updatedAt);
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.filter((bestPractice) => bestPractice.learning > 0 || bestPractice.done > 0);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActivityCounters
|
||||
@@ -107,10 +88,10 @@ export function ActivityPage() {
|
||||
/>
|
||||
|
||||
<div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
|
||||
{learningRoadmapsToShow.length === 0 &&
|
||||
learningBestPracticesToShow.length === 0 && <EmptyActivity />}
|
||||
{learningRoadmaps.length === 0 &&
|
||||
learningBestPractices.length === 0 && <EmptyActivity />}
|
||||
|
||||
{(learningRoadmapsToShow.length > 0 || learningBestPracticesToShow.length > 0) && (
|
||||
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
|
||||
<>
|
||||
<h2 className="mb-3 text-xs uppercase text-gray-400">
|
||||
Continue Following
|
||||
@@ -123,38 +104,26 @@ export function ActivityPage() {
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.filter((roadmap) => roadmap.learning > 0 || roadmap.done > 0)
|
||||
.map((roadmap) => {
|
||||
const learningCount = roadmap.learning || 0;
|
||||
const doneCount = roadmap.done || 0;
|
||||
const totalCount = roadmap.total || 0;
|
||||
const skippedCount = roadmap.skipped || 0;
|
||||
|
||||
return (
|
||||
<ResourceProgress
|
||||
key={roadmap.id}
|
||||
isCustomResource={roadmap.isCustomResource}
|
||||
doneCount={
|
||||
doneCount > totalCount ? totalCount : doneCount
|
||||
}
|
||||
learningCount={
|
||||
learningCount > totalCount ? totalCount : learningCount
|
||||
}
|
||||
totalCount={totalCount}
|
||||
skippedCount={skippedCount}
|
||||
resourceId={roadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.title}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
.map((roadmap) => (
|
||||
<ResourceProgress
|
||||
key={roadmap.id}
|
||||
isCustomResource={roadmap.isCustomResource}
|
||||
doneCount={roadmap.done || 0}
|
||||
learningCount={roadmap.learning || 0}
|
||||
totalCount={roadmap.total || 0}
|
||||
skippedCount={roadmap.skipped || 0}
|
||||
resourceId={roadmap.id}
|
||||
resourceType={'roadmap'}
|
||||
updatedAt={roadmap.updatedAt}
|
||||
title={roadmap.title}
|
||||
onCleared={() => {
|
||||
pageProgressMessage.set('Updating activity');
|
||||
loadActivity().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{learningBestPractices
|
||||
.sort((a, b) => {
|
||||
@@ -163,10 +132,6 @@ export function ActivityPage() {
|
||||
|
||||
return updatedAtB.getTime() - updatedAtA.getTime();
|
||||
})
|
||||
.filter(
|
||||
(bestPractice) =>
|
||||
bestPractice.learning > 0 || bestPractice.done > 0,
|
||||
)
|
||||
.map((bestPractice) => (
|
||||
<ResourceProgress
|
||||
isCustomResource={bestPractice.isCustomResource}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpPatch } from '../../lib/http';
|
||||
import { setAuthToken } from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
import { getUrlParams } from '../../lib/browser';
|
||||
import { CheckIcon } from '../ReactIcons/CheckIcon';
|
||||
|
||||
export function TriggerVerifyEmail() {
|
||||
const { code } = getUrlParams() as { code: string };
|
||||
|
||||
// const [isLoading, setIsLoading] = useState(true);
|
||||
const [status, setStatus] = useState<'loading' | 'error' | 'success'>(
|
||||
'loading',
|
||||
);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const triggerVerify = (code: string) => {
|
||||
setStatus('loading');
|
||||
|
||||
httpPatch<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-new-email/${code}`,
|
||||
{},
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
setError(error?.message || 'Something went wrong. Please try again.');
|
||||
setStatus('error');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setAuthToken(response.token);
|
||||
setStatus('success');
|
||||
})
|
||||
.catch((err) => {
|
||||
setStatus('error');
|
||||
setError('Something went wrong. Please try again.');
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) {
|
||||
setStatus('error');
|
||||
setError('Something went wrong. Please try again later.');
|
||||
return;
|
||||
}
|
||||
|
||||
triggerVerify(code);
|
||||
}, [code]);
|
||||
|
||||
const isLoading = status === 'loading';
|
||||
if (status === 'success') {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12">
|
||||
<CheckIcon additionalClasses={'h-16 w-16 opacity-100'} />
|
||||
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl">
|
||||
Email Update Successful
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base">
|
||||
Your email has been changed successfully. Happy learning!
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
{isLoading && <Spinner className="mx-auto h-16 w-16" />}
|
||||
{error && <ErrorIcon2 className="mx-auto h-16 w-16" />}
|
||||
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl">
|
||||
Verifying your new Email
|
||||
</h2>
|
||||
<div className="text-sm sm:text-base">
|
||||
{isLoading && <p>Please wait while we verify your new Email..</p>}
|
||||
{error && <p className="text-red-700">{error}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getUrlParams } from '../../lib/browser';
|
||||
import { type AppError, type FetchError, httpGet } from '../../lib/http';
|
||||
import {
|
||||
type AppError,
|
||||
type FetchError,
|
||||
httpGet,
|
||||
httpPost,
|
||||
} from '../../lib/http';
|
||||
import { RoadmapHeader } from './RoadmapHeader';
|
||||
import { TopicDetail } from '../TopicDetail/TopicDetail';
|
||||
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
|
||||
import { currentRoadmap } from '../../stores/roadmap';
|
||||
import { RestrictedPage } from './RestrictedPage';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { FlowRoadmapRenderer } from './FlowRoadmapRenderer';
|
||||
|
||||
export const allowedLinkTypes = [
|
||||
|
||||
@@ -125,32 +125,6 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChecklistCheckboxClick = useCallback(
|
||||
(e: MouseEvent, checklistId: string) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusDone = target?.classList.contains('done');
|
||||
updateTopicStatus(checklistId, isCurrentStatusDone ? 'pending' : 'done');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChecklistLabelClick = useCallback(
|
||||
(e: MouseEvent, checklistId: string) => {
|
||||
const target = e?.currentTarget as HTMLDivElement;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentStatusDone = target?.classList.contains('done');
|
||||
updateTopicStatus(checklistId, isCurrentStatusDone ? 'pending' : 'done');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{hideRenderer && (
|
||||
@@ -188,8 +162,6 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
onTopicAltClick={handleTopicAltClick}
|
||||
onButtonNodeClick={handleLinkClick}
|
||||
onLinkClick={handleLinkClick}
|
||||
onChecklistCheckboxClick={handleChecklistCheckboxClick}
|
||||
onChecklistLableClick={handleChecklistLabelClick}
|
||||
fontFamily="Balsamiq Sans"
|
||||
fontURL="/fonts/balsamiq.woff2"
|
||||
/>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { AIRoadmapDocument } from './ExploreAIRoadmap.tsx';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { getRelativeTimeString } from '../../lib/date.ts';
|
||||
|
||||
export type ExploreRoadmapsResponse = {
|
||||
data: AIRoadmapDocument[];
|
||||
totalCount: number;
|
||||
totalPages: number;
|
||||
currPage: number;
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
type AIRoadmapsListProps = {
|
||||
response: ExploreRoadmapsResponse | null;
|
||||
};
|
||||
|
||||
export function AIRoadmapsList(props: AIRoadmapsListProps) {
|
||||
const { response } = props;
|
||||
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roadmaps = response.data || [];
|
||||
|
||||
return (
|
||||
<ul className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{roadmaps.map((roadmap) => {
|
||||
const roadmapLink = `/ai?id=${roadmap._id}`;
|
||||
return (
|
||||
<a
|
||||
key={roadmap._id}
|
||||
href={roadmapLink}
|
||||
className="flex min-h-[95px] flex-col rounded-md border transition-colors hover:bg-gray-100"
|
||||
target={'_blank'}
|
||||
>
|
||||
<h2 className="flex-grow px-2.5 py-2.5 text-base font-medium leading-tight">
|
||||
{roadmap.title}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between gap-2 px-2.5 py-2">
|
||||
<span className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
<Eye size={15} className="inline-block" />
|
||||
{Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
}).format(roadmap.viewCount)}{' '}
|
||||
views
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
{getRelativeTimeString(String(roadmap?.createdAt))}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Map, Wand2 } from 'lucide-react';
|
||||
|
||||
export function EmptyRoadmaps() {
|
||||
return (
|
||||
<div className="flex min-h-[250px] flex-col items-center justify-center rounded-xl border px-5 py-3 sm:px-0 sm:py-20">
|
||||
<Wand2 className="mb-4 h-8 w-8 opacity-10 sm:h-14 sm:w-14" />
|
||||
<h2 className="mb-1 text-lg font-semibold sm:text-xl">
|
||||
No Roadmaps Found
|
||||
</h2>
|
||||
<p className="mb-3 text-balance text-center text-xs text-gray-800 sm:text-sm">
|
||||
Try searching for something else or create a new roadmap with AI.
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-1 sm:flex-row sm:gap-1.5">
|
||||
<a
|
||||
href="/ai"
|
||||
className="flex w-full items-center gap-1.5 rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white sm:w-auto sm:text-sm"
|
||||
>
|
||||
<Wand2 className="h-4 w-4" />
|
||||
Create one with AI
|
||||
</a>
|
||||
<a
|
||||
href="/roadmaps"
|
||||
className="flex w-full items-center gap-1.5 rounded-md bg-yellow-400 px-3 py-1.5 text-xs text-black hover:bg-yellow-500 sm:w-auto sm:text-sm"
|
||||
>
|
||||
<Map className="h-4 w-4" />
|
||||
Visit Official Roadmaps
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { getRelativeTimeString } from '../../lib/date';
|
||||
import { Eye, Loader2, RefreshCcw } from 'lucide-react';
|
||||
import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx';
|
||||
import { ExploreAISorting, type SortByValues } from './ExploreAISorting.tsx';
|
||||
import {
|
||||
deleteUrlParam,
|
||||
getUrlParams,
|
||||
setUrlParams,
|
||||
} from '../../lib/browser.ts';
|
||||
import { Pagination } from '../Pagination/Pagination.tsx';
|
||||
import { LoadingRoadmaps } from './LoadingRoadmaps.tsx';
|
||||
import { EmptyRoadmaps } from './EmptyRoadmaps.tsx';
|
||||
import { AIRoadmapsList } from './AIRoadmapsList.tsx';
|
||||
import { ExploreAISearch } from './ExploreAISearch.tsx';
|
||||
|
||||
export interface AIRoadmapDocument {
|
||||
_id?: string;
|
||||
@@ -32,154 +23,127 @@ type ExploreRoadmapsResponse = {
|
||||
perPage: number;
|
||||
};
|
||||
|
||||
type QueryParams = {
|
||||
q?: string;
|
||||
s?: SortByValues;
|
||||
p?: string;
|
||||
};
|
||||
|
||||
type PageState = {
|
||||
searchTerm: string;
|
||||
sortBy: SortByValues;
|
||||
currentPage: number;
|
||||
};
|
||||
|
||||
export function ExploreAIRoadmap() {
|
||||
const toast = useToast();
|
||||
|
||||
const [pageState, setPageState] = useState<PageState>({
|
||||
searchTerm: '',
|
||||
sortBy: 'createdAt',
|
||||
currentPage: 0,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [roadmaps, setRoadmaps] = useState<AIRoadmapDocument[]>([]);
|
||||
const [currPage, setCurrPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
|
||||
const loadAIRoadmaps = useCallback(
|
||||
async (currPage: number) => {
|
||||
const { response, error } = await httpGet<ExploreRoadmapsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`,
|
||||
{
|
||||
currPage,
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
const newRoadmaps = [...roadmaps, ...response.data];
|
||||
if (
|
||||
JSON.stringify(roadmaps) === JSON.stringify(response.data) ||
|
||||
JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRoadmaps(newRoadmaps);
|
||||
setCurrPage(response.currPage);
|
||||
setTotalPages(response.totalPages);
|
||||
},
|
||||
[currPage, roadmaps],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams = getUrlParams() as QueryParams;
|
||||
|
||||
setPageState({
|
||||
searchTerm: queryParams.q || '',
|
||||
sortBy: queryParams.s || 'createdAt',
|
||||
currentPage: +(queryParams.p || '1'),
|
||||
loadAIRoadmaps(currPage).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
if (!pageState.currentPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only set the URL params if the user modified anything
|
||||
if (
|
||||
pageState.currentPage !== 1 ||
|
||||
pageState.searchTerm !== '' ||
|
||||
pageState.sortBy !== 'createdAt'
|
||||
) {
|
||||
setUrlParams({
|
||||
q: pageState.searchTerm,
|
||||
s: pageState.sortBy,
|
||||
p: String(pageState.currentPage),
|
||||
});
|
||||
} else {
|
||||
deleteUrlParam('q');
|
||||
deleteUrlParam('s');
|
||||
deleteUrlParam('p');
|
||||
}
|
||||
|
||||
loadAIRoadmaps(
|
||||
pageState.currentPage,
|
||||
pageState.searchTerm,
|
||||
pageState.sortBy,
|
||||
).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [pageState]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [roadmapsResponse, setRoadmapsResponse] =
|
||||
useState<ExploreRoadmapsResponse | null>(null);
|
||||
|
||||
const loadAIRoadmaps = async (
|
||||
currPage: number = 1,
|
||||
searchTerm: string = '',
|
||||
sortBy: SortByValues = 'createdAt',
|
||||
) => {
|
||||
const { response, error } = await httpGet<ExploreRoadmapsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`,
|
||||
{
|
||||
currPage,
|
||||
...(searchTerm && { term: searchTerm }),
|
||||
...(sortBy && { sortBy }),
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
setRoadmapsResponse(response);
|
||||
};
|
||||
|
||||
const roadmaps = roadmapsResponse?.data || [];
|
||||
|
||||
const loadingIndicator = isLoading && <LoadingRoadmaps />;
|
||||
const emptyRoadmaps = !isLoading && roadmaps.length === 0 && (
|
||||
<EmptyRoadmaps />
|
||||
);
|
||||
|
||||
const roadmapsList = !isLoading && roadmaps.length > 0 && (
|
||||
<>
|
||||
<AIRoadmapsList response={roadmapsResponse} />
|
||||
<Pagination
|
||||
currPage={roadmapsResponse?.currPage || 1}
|
||||
totalPages={roadmapsResponse?.totalPages || 1}
|
||||
perPage={roadmapsResponse?.perPage || 0}
|
||||
isDisabled={isLoading}
|
||||
totalCount={roadmapsResponse?.totalCount || 0}
|
||||
onPageChange={(page) => {
|
||||
setPageState({
|
||||
...pageState,
|
||||
currentPage: page,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const hasMorePages = currPage < totalPages;
|
||||
|
||||
return (
|
||||
<section className="container mx-auto py-3 sm:py-6">
|
||||
<AIRoadmapAlert isListing />
|
||||
|
||||
<div className="my-3.5 flex items-stretch justify-between gap-2.5">
|
||||
<ExploreAISearch
|
||||
total={roadmapsResponse?.totalCount || 0}
|
||||
isLoading={isLoading}
|
||||
value={pageState.searchTerm}
|
||||
onSubmit={(term: string) => {
|
||||
setPageState({
|
||||
...pageState,
|
||||
searchTerm: term,
|
||||
currentPage: 1,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<ExploreAISorting
|
||||
sortBy={pageState.sortBy}
|
||||
onSortChange={(sortBy) => {
|
||||
setPageState({
|
||||
...pageState,
|
||||
sortBy,
|
||||
currentPage: 1,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="mb-6">
|
||||
<AIRoadmapAlert isListing />
|
||||
</div>
|
||||
|
||||
{loadingIndicator}
|
||||
{emptyRoadmaps}
|
||||
{roadmapsList}
|
||||
{isLoading ? (
|
||||
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{new Array(21).fill(0).map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="h-[75px] animate-pulse rounded-md border bg-gray-100"
|
||||
></li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div>
|
||||
{roadmaps?.length === 0 ? (
|
||||
<div className="text-center text-gray-800">No roadmaps found</div>
|
||||
) : (
|
||||
<>
|
||||
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{roadmaps.map((roadmap) => {
|
||||
const roadmapLink = `/ai?id=${roadmap._id}`;
|
||||
return (
|
||||
<a
|
||||
key={roadmap._id}
|
||||
href={roadmapLink}
|
||||
className="flex flex-col rounded-md border transition-colors hover:bg-gray-100"
|
||||
target={'_blank'}
|
||||
>
|
||||
<h2 className="flex-grow px-2.5 py-2.5 text-base font-medium leading-tight">
|
||||
{roadmap.title}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between gap-2 px-2.5 py-2">
|
||||
<span className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
<Eye size={15} className="inline-block" />
|
||||
{Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
}).format(roadmap.viewCount)}{' '}
|
||||
views
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
{getRelativeTimeString(String(roadmap?.createdAt))}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{hasMorePages && (
|
||||
<div className="my-5 flex items-center justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLoadingMore(true);
|
||||
loadAIRoadmaps(currPage + 1).finally(() => {
|
||||
setIsLoadingMore(false);
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 rounded-full bg-black px-3 py-1.5 text-sm font-medium text-white shadow-xl transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin stroke-[2.5]" />
|
||||
) : (
|
||||
<RefreshCcw className="h-4 w-4 stroke-[2.5]" />
|
||||
)}
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Search } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebounceValue } from '../../hooks/use-debounce.ts';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
|
||||
type ExploreAISearchProps = {
|
||||
value: string;
|
||||
total: number;
|
||||
isLoading: boolean;
|
||||
onSubmit: (search: string) => void;
|
||||
};
|
||||
|
||||
export function ExploreAISearch(props: ExploreAISearchProps) {
|
||||
const { onSubmit, isLoading = false, total, value: defaultValue } = props;
|
||||
|
||||
const [term, setTerm] = useState(defaultValue);
|
||||
const debouncedTerm = useDebounceValue(term, 500);
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(defaultValue);
|
||||
}, [defaultValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedTerm && debouncedTerm.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (debouncedTerm === defaultValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(debouncedTerm);
|
||||
}, [debouncedTerm]);
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full items-center gap-3">
|
||||
<div className="relative flex w-full max-w-[310px] items-center">
|
||||
<label
|
||||
className="absolute left-3 flex h-full items-center text-gray-500"
|
||||
htmlFor="search"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</label>
|
||||
<input
|
||||
id="search"
|
||||
name="search"
|
||||
type="text"
|
||||
placeholder="Type 3 or more characters to search..."
|
||||
className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-none"
|
||||
value={term}
|
||||
onChange={(e) => setTerm(e.target.value)}
|
||||
/>
|
||||
{isLoading && (
|
||||
<span className="absolute right-3 top-0 flex h-full items-center text-gray-500">
|
||||
<Spinner isDualRing={false} className={`h-3 w-3`} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{total > 0 && (
|
||||
<p className="flex-shrink-0 text-sm text-gray-500 hidden sm:block">
|
||||
{Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
}).format(total)}{' '}
|
||||
results found
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ArrowDownWideNarrow, Check, ChevronDown } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
|
||||
export type SortByValues = 'viewCount' | 'createdAt' | '-createdAt';
|
||||
const sortingLabels: { label: string; value: SortByValues }[] = [
|
||||
{
|
||||
label: 'Most Viewed',
|
||||
value: 'viewCount',
|
||||
},
|
||||
{
|
||||
label: 'Newest',
|
||||
value: 'createdAt',
|
||||
},
|
||||
{
|
||||
label: 'Oldest',
|
||||
value: '-createdAt',
|
||||
},
|
||||
];
|
||||
|
||||
type ExploreAISortingProps = {
|
||||
sortBy: SortByValues;
|
||||
onSortChange: (sortBy: SortByValues) => void;
|
||||
};
|
||||
|
||||
export function ExploreAISorting(props: ExploreAISortingProps) {
|
||||
const { sortBy, onSortChange } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const selectedValue = sortingLabels.find((item) => item.value === sortBy);
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-auto relative flex flex-shrink-0 sm:min-w-[140px]"
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<button
|
||||
className="py-15 flex w-full items-center justify-between gap-2 rounded-md border px-2 text-sm"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span>{selectedValue?.label}</span>
|
||||
|
||||
<span>
|
||||
<ChevronDown className="ml-4 h-3.5 w-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 top-10 z-10 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
{sortingLabels.map((item) => (
|
||||
<button
|
||||
key={item.value}
|
||||
className="flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
onSortChange(item.value);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{item.value === sortBy && <Check className="ml-auto h-4 w-4" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export function LoadingRoadmaps() {
|
||||
return (
|
||||
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{new Array(21).fill(0).map((_, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="h-[95px] animate-pulse rounded-md border bg-gray-100"
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -48,11 +48,10 @@ const {
|
||||
{
|
||||
isNew && (
|
||||
<span class='absolute bottom-1.5 right-2 flex items-center rounded-br rounded-tl text-xs font-medium text-purple-300'>
|
||||
<span class='mr-1.5 flex h-2 w-2'>
|
||||
<span class='flex h-2 w-2'>
|
||||
<span class='absolute inline-flex h-2 w-2 animate-ping rounded-full bg-purple-400 opacity-75' />
|
||||
<span class='relative inline-flex h-2 w-2 rounded-full bg-purple-500' />
|
||||
</span>
|
||||
New
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,10 +48,10 @@ import Icon from './AstroIcon.astro';
|
||||
<span class='mx-2 text-gray-400'>by</span>
|
||||
<a
|
||||
class='font-regular rounded-md bg-blue-600 px-1.5 py-1 text-sm hover:bg-blue-700'
|
||||
href='https://kamranahmed.info'
|
||||
href='https://twitter.com/kamrify'
|
||||
target='_blank'
|
||||
>
|
||||
<span class='hidden sm:inline'>Kamran</span>
|
||||
<span class='hidden sm:inline'>@kamrify</span>
|
||||
<span class='inline sm:hidden'>Kamran Ahmed</span>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
import {setUrlParams} from "../../lib/browser.ts";
|
||||
|
||||
export class Renderer {
|
||||
resourceId: string;
|
||||
@@ -142,8 +141,19 @@ export class Renderer {
|
||||
|
||||
const newJsonFileSlug = newJsonUrl.split('/').pop()?.replace('.json', '');
|
||||
|
||||
const type = this.resourceType[0]; // r for roadmap, b for best-practices
|
||||
setUrlParams({ [type]: newJsonFileSlug! })
|
||||
// Update the URL and attach the new roadmap type
|
||||
if (window?.history?.pushState) {
|
||||
const url = new URL(window.location.href);
|
||||
const type = this.resourceType[0]; // r for roadmap, b for best-practices
|
||||
|
||||
url.searchParams.delete(type);
|
||||
|
||||
if (newJsonFileSlug !== this.resourceId) {
|
||||
url.searchParams.set(type, newJsonFileSlug!);
|
||||
}
|
||||
|
||||
window.history.pushState(null, '', url.toString());
|
||||
}
|
||||
|
||||
this.jsonToSvg(newJsonUrl)?.then(() => {});
|
||||
}
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
import {
|
||||
type InputHTMLAttributes,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useDebounceValue } from '../../hooks/use-debounce';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu.tsx';
|
||||
|
||||
type GetTopAIRoadmapTermResponse = {
|
||||
_id: string;
|
||||
term: string;
|
||||
title: string;
|
||||
isOfficial: boolean;
|
||||
}[];
|
||||
|
||||
type AITermSuggestionInputProps = {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
onSelect?: (roadmapId: string, roadmapTitle: string) => void;
|
||||
inputClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
placeholder?: string;
|
||||
} & Omit<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
'onSelect' | 'onChange' | 'className'
|
||||
>;
|
||||
|
||||
export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
|
||||
const {
|
||||
value: defaultValue,
|
||||
onValueChange,
|
||||
onSelect,
|
||||
inputClassName,
|
||||
wrapperClassName,
|
||||
placeholder,
|
||||
...inputProps
|
||||
} = props;
|
||||
|
||||
const termCache = useMemo(
|
||||
() => new Map<string, GetTopAIRoadmapTermResponse>(),
|
||||
[],
|
||||
);
|
||||
const [officialRoadmaps, setOfficialRoadmaps] =
|
||||
useState<GetTopAIRoadmapTermResponse>([]);
|
||||
|
||||
const toast = useToast();
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchResults, setSearchResults] =
|
||||
useState<GetTopAIRoadmapTermResponse>([]);
|
||||
const [searchedText, setSearchedText] = useState(defaultValue);
|
||||
const [activeCounter, setActiveCounter] = useState(0);
|
||||
const debouncedSearchValue = useDebounceValue(searchedText, 300);
|
||||
|
||||
const loadTopAIRoadmapTerm = async () => {
|
||||
const trimmedValue = debouncedSearchValue.trim();
|
||||
if (trimmedValue.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (termCache.has(trimmedValue)) {
|
||||
const cachedData = termCache.get(trimmedValue);
|
||||
return cachedData || [];
|
||||
}
|
||||
|
||||
const { response, error } = await httpGet<GetTopAIRoadmapTermResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-top-ai-roadmap-term`,
|
||||
{
|
||||
term: trimmedValue,
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
setSearchResults([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
termCache.set(trimmedValue, response);
|
||||
return response;
|
||||
};
|
||||
|
||||
const loadOfficialRoadmaps = async () => {
|
||||
if (officialRoadmaps.length > 0) {
|
||||
return officialRoadmaps;
|
||||
}
|
||||
|
||||
const { error, response } = await httpGet<PageType[]>(`/pages.json`);
|
||||
|
||||
if (error) {
|
||||
toast.error(error.message || 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allRoadmaps = response
|
||||
.filter((page) => page.group === 'Roadmaps')
|
||||
.sort((a, b) => {
|
||||
if (a.title === 'Android') return 1;
|
||||
return a.title.localeCompare(b.title);
|
||||
})
|
||||
.map((page) => ({
|
||||
_id: page.id,
|
||||
term: page.title,
|
||||
title: page.title,
|
||||
isOfficial: true,
|
||||
}));
|
||||
|
||||
setOfficialRoadmaps(allRoadmaps);
|
||||
return allRoadmaps;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearchValue.length === 0 || isFirstRender.current) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsActive(true);
|
||||
setIsLoading(true);
|
||||
loadTopAIRoadmapTerm()
|
||||
.then((results) => {
|
||||
const normalizedSearchText = debouncedSearchValue.trim().toLowerCase();
|
||||
const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => {
|
||||
return (
|
||||
roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1
|
||||
);
|
||||
});
|
||||
|
||||
setSearchResults(
|
||||
[...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [],
|
||||
);
|
||||
setActiveCounter(0);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [debouncedSearchValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
loadOfficialRoadmaps().finally(() => {});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useOutsideClick(dropdownRef, () => {
|
||||
setIsActive(false);
|
||||
});
|
||||
|
||||
const isFinishedTyping = debouncedSearchValue === searchedText;
|
||||
|
||||
return (
|
||||
<div className={cn('relative', wrapperClassName)}>
|
||||
<input
|
||||
{...inputProps}
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={defaultValue}
|
||||
className={cn(
|
||||
'w-full rounded-md border border-gray-400 px-3 py-2.5 pr-8 transition-colors focus:border-black focus:outline-none',
|
||||
inputClassName,
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
onChange={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
setSearchedText(value);
|
||||
onValueChange(value);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setIsActive(true);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
const canGoNext = activeCounter < searchResults.length - 1;
|
||||
setActiveCounter(canGoNext ? activeCounter + 1 : 0);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
const canGoPrev = activeCounter > 0;
|
||||
setActiveCounter(
|
||||
canGoPrev ? activeCounter - 1 : searchResults.length - 1,
|
||||
);
|
||||
} else if (e.key === 'Tab') {
|
||||
if (isActive) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
setSearchedText('');
|
||||
setIsActive(false);
|
||||
} else if (e.key === 'Enter') {
|
||||
if (!searchResults.length || !isFinishedTyping) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const activeData = searchResults[activeCounter];
|
||||
if (activeData) {
|
||||
if (activeData.isOfficial) {
|
||||
window.open(`/${activeData._id}`, '_blank')?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
onValueChange(activeData.term);
|
||||
onSelect?.(activeData._id, activeData.title);
|
||||
setIsActive(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute right-3 top-0 flex h-full items-center">
|
||||
<Spinner
|
||||
isDualRing={false}
|
||||
className="h-5 w-5 animate-spin stroke-[2.5]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isActive &&
|
||||
isFinishedTyping &&
|
||||
searchResults.length > 0 &&
|
||||
searchedText.length > 0 && (
|
||||
<div
|
||||
className="absolute top-full z-50 mt-1 w-full rounded-md border bg-white p-1 shadow"
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
{searchResults.map((result, counter) => {
|
||||
return (
|
||||
<button
|
||||
key={result?._id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex w-full items-start rounded p-2 text-left text-sm',
|
||||
counter === activeCounter ? 'bg-gray-100' : '',
|
||||
)}
|
||||
onMouseOver={() => setActiveCounter(counter)}
|
||||
onClick={() => {
|
||||
if (result.isOfficial) {
|
||||
window.location.href = `/${result._id}`;
|
||||
return;
|
||||
}
|
||||
|
||||
onValueChange(result?.term);
|
||||
onSelect?.(result._id, result.title);
|
||||
setSearchedText('');
|
||||
setIsActive(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'mr-2 whitespace-nowrap rounded-full p-1 px-1.5 text-xs leading-none',
|
||||
result.isOfficial
|
||||
? 'bg-green-500 text-green-50'
|
||||
: 'bg-blue-400 text-blue-50',
|
||||
)}
|
||||
>
|
||||
{result.isOfficial ? 'Official' : 'AI Generated'}
|
||||
</span>
|
||||
{result?.title || result?.term}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
getOpenAIKey,
|
||||
isLoggedIn,
|
||||
removeAuthToken,
|
||||
setAIReferralCode,
|
||||
visitAIRoadmap,
|
||||
} from '../../lib/jwt';
|
||||
import { RoadmapSearch } from './RoadmapSearch.tsx';
|
||||
@@ -37,10 +36,6 @@ import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
|
||||
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
|
||||
import { OpenAISettings } from './OpenAISettings.tsx';
|
||||
import { IS_KEY_ONLY_ROADMAP_GENERATION } from '../../lib/ai.ts';
|
||||
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
|
||||
import { useParams } from '../../hooks/use-params.ts';
|
||||
import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
|
||||
import { AuthenticationForm } from '../AuthenticationFlow/AuthenticationForm.tsx';
|
||||
|
||||
export type GetAIRoadmapLimitResponse = {
|
||||
used: number;
|
||||
@@ -90,15 +85,11 @@ type GetAIRoadmapResponse = {
|
||||
export function GenerateRoadmap() {
|
||||
const roadmapContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { id: roadmapId, rc: referralCode } = getUrlParams() as {
|
||||
id: string;
|
||||
rc?: string;
|
||||
};
|
||||
const { id: roadmapId } = getUrlParams() as { id: string };
|
||||
const toast = useToast();
|
||||
|
||||
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingResults, setIsLoadingResults] = useState(false);
|
||||
const [roadmapTerm, setRoadmapTerm] = useState('');
|
||||
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
|
||||
const [currentRoadmap, setCurrentRoadmap] =
|
||||
@@ -113,9 +104,7 @@ export function GenerateRoadmap() {
|
||||
const [roadmapTopicLimitUsed, setRoadmapTopicLimitUsed] = useState(0);
|
||||
const [isConfiguring, setIsConfiguring] = useState(false);
|
||||
|
||||
const [openAPIKey, setOpenAPIKey] = useState<string | undefined>(
|
||||
getOpenAIKey(),
|
||||
);
|
||||
const openAPIKey = getOpenAIKey();
|
||||
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
|
||||
const isAuthenticatedUser = isLoggedIn();
|
||||
|
||||
@@ -131,6 +120,12 @@ export function GenerateRoadmap() {
|
||||
setIsLoading(true);
|
||||
setHasSubmitted(true);
|
||||
|
||||
if (roadmapLimitUsed >= roadmapLimit) {
|
||||
toast.error('You have reached your limit of generating roadmaps');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
deleteUrlParam('id');
|
||||
setCurrentRoadmap(null);
|
||||
|
||||
@@ -176,13 +171,10 @@ export function GenerateRoadmap() {
|
||||
const roadmapId = result.match(ROADMAP_ID_REGEX)?.[1] || '';
|
||||
setUrlParams({ id: roadmapId });
|
||||
result = result.replace(ROADMAP_ID_REGEX, '');
|
||||
const roadmapTitle =
|
||||
result.trim().split('\n')[0]?.replace('#', '')?.trim() || term;
|
||||
setRoadmapTerm(roadmapTitle);
|
||||
setCurrentRoadmap({
|
||||
id: roadmapId,
|
||||
term: roadmapTerm,
|
||||
title: roadmapTitle,
|
||||
title: term,
|
||||
data: result,
|
||||
});
|
||||
}
|
||||
@@ -201,11 +193,11 @@ export function GenerateRoadmap() {
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!roadmapTerm || isLoadingResults) {
|
||||
if (!roadmapTerm) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roadmapTerm === currentRoadmap?.term) {
|
||||
if (roadmapTerm === currentRoadmap?.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -301,8 +293,7 @@ export function GenerateRoadmap() {
|
||||
pageProgressMessage.set('Loading Roadmap');
|
||||
|
||||
const { response, error } = await httpGet<{
|
||||
term: string;
|
||||
title: string;
|
||||
topic: string;
|
||||
data: string;
|
||||
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap/${roadmapId}`);
|
||||
|
||||
@@ -322,7 +313,7 @@ export function GenerateRoadmap() {
|
||||
data,
|
||||
});
|
||||
|
||||
setRoadmapTerm(term);
|
||||
setRoadmapTerm(title);
|
||||
setGeneratedRoadmapContent(data);
|
||||
visitAIRoadmap(roadmapId);
|
||||
};
|
||||
@@ -369,17 +360,6 @@ export function GenerateRoadmap() {
|
||||
loadAIRoadmapLimit().finally(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!referralCode || isLoggedIn()) {
|
||||
deleteUrlParam('rc');
|
||||
return;
|
||||
}
|
||||
|
||||
setAIReferralCode(referralCode);
|
||||
deleteUrlParam('rc');
|
||||
showLoginPopup();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!roadmapId || roadmapId === currentRoadmap?.id) {
|
||||
return;
|
||||
@@ -411,13 +391,13 @@ export function GenerateRoadmap() {
|
||||
|
||||
const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`;
|
||||
const canGenerateMore = roadmapLimitUsed < roadmapLimit;
|
||||
const isLoggedInUser = isLoggedIn();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isConfiguring && (
|
||||
<IncreaseRoadmapLimit
|
||||
<OpenAISettings
|
||||
onClose={() => {
|
||||
setOpenAPIKey(getOpenAIKey());
|
||||
setIsConfiguring(false);
|
||||
loadAIRoadmapLimit().finally(() => null);
|
||||
}}
|
||||
@@ -499,15 +479,17 @@ export function GenerateRoadmap() {
|
||||
>
|
||||
{roadmapLimitUsed} of {roadmapLimit}
|
||||
</span>{' '}
|
||||
roadmaps generated today.
|
||||
roadmaps generated.
|
||||
</span>
|
||||
{!openAPIKey && (
|
||||
<button
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-left text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
Need to generate more?{' '}
|
||||
<span className="font-semibold">Click here.</span>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -534,14 +516,15 @@ export function GenerateRoadmap() {
|
||||
onSubmit={handleSubmit}
|
||||
className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center"
|
||||
>
|
||||
<AITermSuggestionInput
|
||||
value={roadmapTerm}
|
||||
onValueChange={(value) => setRoadmapTerm(value)}
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
placeholder="e.g. Try searching for Ansible or DevOps"
|
||||
wrapperClassName="grow"
|
||||
onSelect={(id, title) => {
|
||||
loadTermRoadmap(title).finally(() => null);
|
||||
}}
|
||||
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
|
||||
value={roadmapTerm}
|
||||
onInput={(e) =>
|
||||
setRoadmapTerm((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type={'submit'}
|
||||
@@ -556,47 +539,37 @@ export function GenerateRoadmap() {
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isLoadingResults ||
|
||||
(isAuthenticatedUser &&
|
||||
(!roadmapLimit ||
|
||||
!roadmapTerm ||
|
||||
roadmapLimitUsed >= roadmapLimit ||
|
||||
roadmapTerm === currentRoadmap?.term ||
|
||||
(isKeyOnly && !openAPIKey)))
|
||||
isAuthenticatedUser &&
|
||||
(!roadmapLimit ||
|
||||
!roadmapTerm ||
|
||||
roadmapLimitUsed >= roadmapLimit ||
|
||||
roadmapTerm === currentRoadmap?.term ||
|
||||
(isKeyOnly && !openAPIKey))
|
||||
}
|
||||
>
|
||||
{isLoadingResults && (
|
||||
{!isAuthenticatedUser && (
|
||||
<>
|
||||
<span>Please wait..</span>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
{!isLoadingResults && (
|
||||
|
||||
{isAuthenticatedUser && (
|
||||
<>
|
||||
{!isAuthenticatedUser && (
|
||||
{roadmapLimit > 0 && canGenerateMore && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAuthenticatedUser && (
|
||||
<>
|
||||
{roadmapLimit > 0 && canGenerateMore && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
{roadmapLimit === 0 && <span>Please wait..</span>}
|
||||
|
||||
{roadmapLimit === 0 && <span>Please wait..</span>}
|
||||
|
||||
{roadmapLimit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
{roadmapLimit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -659,48 +632,11 @@ export function GenerateRoadmap() {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn({
|
||||
'relative mb-20 max-h-[800px] min-h-[800px] overflow-hidden sm:max-h-[1000px] md:min-h-[1000px] lg:max-h-[1200px] lg:min-h-[1200px]':
|
||||
!isAuthenticatedUser,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
ref={roadmapContainerRef}
|
||||
id="roadmap-container"
|
||||
onClick={handleNodeClick}
|
||||
className="relative min-h-[400px] px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
|
||||
/>
|
||||
{!isAuthenticatedUser && (
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
<div className="h-80 w-full bg-gradient-to-t from-gray-100 to-transparent" />
|
||||
<div className="bg-gray-100">
|
||||
<div className="mx-auto max-w-[600px] flex-col items-center justify-center bg-gray-100 px-5 pt-px">
|
||||
<div className="mt-8 text-center">
|
||||
<h2 className="mb-0.5 text-xl font-medium sm:mb-3 sm:text-2xl">
|
||||
Sign up to View the full roadmap
|
||||
</h2>
|
||||
<p className="mb-6 text-balance text-sm text-gray-600 sm:text-base">
|
||||
You must be logged in to view the complete roadmap
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-[350px]">
|
||||
<AuthenticationForm type="signup" />
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-600">
|
||||
Already have an account?{' '}
|
||||
<a
|
||||
href="/login"
|
||||
className="font-medium text-blue-700 hover:text-blue-600"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
ref={roadmapContainerRef}
|
||||
id="roadmap-container"
|
||||
onClick={handleNodeClick}
|
||||
className="relative px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import { Modal } from '../Modal';
|
||||
import { ReferYourFriend } from './ReferYourFriend';
|
||||
import { OpenAISettings } from './OpenAISettings';
|
||||
import { PayToBypass } from './PayToBypass';
|
||||
import { PickLimitOption } from './PickLimitOption';
|
||||
import { getOpenAIKey } from '../../lib/jwt.ts';
|
||||
|
||||
export type IncreaseTab = 'api-key' | 'refer-friends' | 'payment';
|
||||
|
||||
export const increaseLimitTabs: {
|
||||
key: IncreaseTab;
|
||||
title: string;
|
||||
}[] = [
|
||||
{ key: 'api-key', title: 'Add your own API Key' },
|
||||
{ key: 'refer-friends', title: 'Refer your Friends' },
|
||||
{ key: 'payment', title: 'Pay to Bypass the limit' },
|
||||
];
|
||||
|
||||
type IncreaseRoadmapLimitProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
export function IncreaseRoadmapLimit(props: IncreaseRoadmapLimitProps) {
|
||||
const { onClose } = props;
|
||||
|
||||
const openAPIKey = getOpenAIKey();
|
||||
const [activeTab, setActiveTab] = useState<IncreaseTab | null>(
|
||||
openAPIKey ? 'api-key' : null,
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
overlayClassName={cn(
|
||||
'overscroll-contain',
|
||||
activeTab === 'payment' && 'block',
|
||||
)}
|
||||
wrapperClassName="max-w-lg mx-auto"
|
||||
bodyClassName={cn('h-auto pt-px', !activeTab && 'overflow-hidden')}
|
||||
>
|
||||
{!activeTab && (
|
||||
<PickLimitOption activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||
)}
|
||||
|
||||
{activeTab === 'api-key' && (
|
||||
<OpenAISettings
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
onBack={() => setActiveTab(null)}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'refer-friends' && (
|
||||
<ReferYourFriend onBack={() => setActiveTab(null)} />
|
||||
)}
|
||||
{activeTab === 'payment' && (
|
||||
<PayToBypass
|
||||
onBack={() => setActiveTab(null)}
|
||||
onClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -5,15 +5,13 @@ import { cn } from '../../lib/classname.ts';
|
||||
import { CloseIcon } from '../ReactIcons/CloseIcon.tsx';
|
||||
import { useToast } from '../../hooks/use-toast.ts';
|
||||
import { httpPost } from '../../lib/http.ts';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
type OpenAISettingsProps = {
|
||||
onClose: () => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export function OpenAISettings(props: OpenAISettingsProps) {
|
||||
const { onClose, onBack } = props;
|
||||
const { onClose } = props;
|
||||
|
||||
const [defaultOpenAIKey, setDefaultOpenAIKey] = useState('');
|
||||
|
||||
@@ -30,143 +28,141 @@ export function OpenAISettings(props: OpenAISettingsProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Back to options
|
||||
</button>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800">OpenAI Settings</h2>
|
||||
<p className="mt-2 text-sm leading-normal text-gray-500">
|
||||
Add your OpenAI API key below to bypass the roadmap generation limits.
|
||||
You can use your existing key or{' '}
|
||||
<a
|
||||
className="underline underline-offset-2 hover:text-gray-900"
|
||||
href={'https://platform.openai.com/signup'}
|
||||
target="_blank"
|
||||
>
|
||||
create a new one here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
<form
|
||||
className="mt-4"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setHasError(false);
|
||||
|
||||
const normalizedKey = openaiApiKey.trim();
|
||||
if (!normalizedKey) {
|
||||
deleteOpenAIKey();
|
||||
toast.success('OpenAI API key removed');
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizedKey.startsWith('sk-')) {
|
||||
setHasError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-validate-openai-key`,
|
||||
{
|
||||
key: normalizedKey,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the API key to cookies
|
||||
saveOpenAIKey(normalizedKey);
|
||||
toast.success('OpenAI API key saved');
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="openai-api-key"
|
||||
id="openai-api-key"
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-gray-300 px-3 py-2 text-gray-800 transition-colors focus:border-black focus:outline-none',
|
||||
{
|
||||
'border-red-500 bg-red-100 focus:border-red-500': hasError,
|
||||
},
|
||||
)}
|
||||
placeholder="Enter your OpenAI API key"
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => {
|
||||
setHasError(false);
|
||||
setOpenaiApiKey((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{openaiApiKey && (
|
||||
<button
|
||||
type={'button'}
|
||||
onClick={() => {
|
||||
setOpenaiApiKey('');
|
||||
}}
|
||||
className="absolute right-2 top-1/2 flex h-[20px] w-[20px] -translate-y-1/2 items-center justify-center rounded-full bg-gray-400 text-white hover:bg-gray-600"
|
||||
>
|
||||
<CloseIcon className="h-[13px] w-[13px] stroke-[3.5]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className={'mb-2 mt-1 text-xs text-gray-500'}>
|
||||
We do not store your API key on our servers.
|
||||
</p>
|
||||
|
||||
{hasError && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
Please enter a valid OpenAI API key
|
||||
<Modal onClose={onClose}>
|
||||
<div className="p-5">
|
||||
<h2 className="text-xl font-medium text-gray-800">OpenAI Settings</h2>
|
||||
<div className="mt-4">
|
||||
<p className="text-gray-700">
|
||||
AI Roadmap generator uses OpenAI's GPT-4 model to generate roadmaps.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type="submit"
|
||||
className={
|
||||
'mt-2 w-full rounded-md bg-gray-700 px-4 py-2 text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-50'
|
||||
}
|
||||
>
|
||||
{!isLoading && 'Save'}
|
||||
{isLoading && 'Validating ..'}
|
||||
</button>
|
||||
{!defaultOpenAIKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
||||
<p className="mt-2">
|
||||
<a
|
||||
className="font-semibold underline underline-offset-2"
|
||||
href={'https://platform.openai.com/signup'}
|
||||
target="_blank"
|
||||
>
|
||||
Create an account on OpenAI
|
||||
</a>{' '}
|
||||
and enter your API key below to enable the AI Roadmap generator
|
||||
</p>
|
||||
|
||||
<form
|
||||
className="mt-4"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setHasError(false);
|
||||
|
||||
const normalizedKey = openaiApiKey.trim();
|
||||
if (!normalizedKey) {
|
||||
deleteOpenAIKey();
|
||||
toast.success('OpenAI API key removed');
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizedKey.startsWith('sk-')) {
|
||||
setHasError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-validate-openai-key`,
|
||||
{
|
||||
key: normalizedKey,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setHasError(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the API key to cookies
|
||||
saveOpenAIKey(normalizedKey);
|
||||
toast.success('OpenAI API key saved');
|
||||
onClose();
|
||||
}}
|
||||
className="mt-1 w-full rounded-md border border-red-500 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-700 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{defaultOpenAIKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
deleteOpenAIKey();
|
||||
onClose();
|
||||
toast.success('OpenAI API key removed');
|
||||
}}
|
||||
className="mt-1 w-full rounded-md border border-red-500 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-700 hover:text-white"
|
||||
>
|
||||
Remove API Key
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="openai-api-key"
|
||||
id="openai-api-key"
|
||||
className={cn(
|
||||
'block w-full rounded-md border border-gray-300 px-3 py-2 text-gray-800 transition-colors focus:border-black focus:outline-none',
|
||||
{
|
||||
'border-red-500 bg-red-100 focus:border-red-500': hasError,
|
||||
},
|
||||
)}
|
||||
placeholder="Enter your OpenAI API key"
|
||||
value={openaiApiKey}
|
||||
onChange={(e) => {
|
||||
setHasError(false);
|
||||
setOpenaiApiKey((e.target as HTMLInputElement).value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{openaiApiKey && (
|
||||
<button
|
||||
type={'button'}
|
||||
onClick={() => {
|
||||
setOpenaiApiKey('');
|
||||
}}
|
||||
className="absolute right-2 top-1/2 flex h-[20px] w-[20px] -translate-y-1/2 items-center justify-center rounded-full bg-gray-400 text-white hover:bg-gray-600"
|
||||
>
|
||||
<CloseIcon className="h-[13px] w-[13px] stroke-[3.5]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className={'mb-2 mt-1 text-xs text-gray-500'}>
|
||||
We do not store your API key on our servers.
|
||||
</p>
|
||||
|
||||
{hasError && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
Please enter a valid OpenAI API key
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type="submit"
|
||||
className={
|
||||
'mt-2 w-full rounded-md bg-gray-700 px-4 py-2 text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-50'
|
||||
}
|
||||
>
|
||||
{!isLoading && 'Save'}
|
||||
{isLoading && 'Validating ..'}
|
||||
</button>
|
||||
{!defaultOpenAIKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
className="mt-1 w-full rounded-md bg-red-500 px-4 py-2 text-white transition-colors hover:bg-black hover:bg-red-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{defaultOpenAIKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
deleteOpenAIKey();
|
||||
onClose();
|
||||
toast.success('OpenAI API key removed');
|
||||
}}
|
||||
className="mt-1 w-full rounded-md bg-red-500 px-4 py-2 text-white transition-colors hover:bg-black hover:bg-red-700"
|
||||
>
|
||||
Reset to Default Key
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
|
||||
type PayToBypassProps = {
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function PayToBypass(props: PayToBypassProps) {
|
||||
const { onBack, onClose } = props;
|
||||
const user = useAuth();
|
||||
|
||||
const userId = 'entry.1665642993';
|
||||
const nameId = 'entry.527005328';
|
||||
const emailId = 'entry.982906376';
|
||||
const amountId = 'entry.1826002937';
|
||||
const roadmapCountId = 'entry.1161404075';
|
||||
const usageId = 'entry.535914744';
|
||||
const feedbackId = 'entry.1024388959';
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Back to options
|
||||
</button>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800">Pay to Bypass</h2>
|
||||
<p className="mt-2 text-sm leading-normal text-gray-500">
|
||||
Tell us more about how you will be using this.
|
||||
</p>
|
||||
|
||||
<form
|
||||
className="mt-4 flex flex-col gap-3"
|
||||
action="https://docs.google.com/forms/u/0/d/e/1FAIpQLSeec1oboTc9vCWHxmoKsC5NIbACpQEk7erp8wBKJMz-nzC7LQ/formResponse"
|
||||
target="_blank"
|
||||
>
|
||||
<div className="sr-only" aria-hidden="true">
|
||||
<label htmlFor={userId} className="sr-only">
|
||||
User Id
|
||||
</label>
|
||||
<input
|
||||
id={userId}
|
||||
name={userId}
|
||||
type="text"
|
||||
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"
|
||||
value={user?.id}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="sr-only" aria-hidden="true">
|
||||
<label htmlFor={nameId} className="sr-only">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id={nameId}
|
||||
name={nameId}
|
||||
type="text"
|
||||
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"
|
||||
value={user?.name}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
<div className="sr-only" aria-hidden="true">
|
||||
<label htmlFor={emailId} className="sr-only">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id={emailId}
|
||||
name={emailId}
|
||||
type="email"
|
||||
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"
|
||||
value={user?.email}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor={amountId}
|
||||
className="mb-2 block text-sm font-semibold"
|
||||
>
|
||||
How much are you willing to pay for this? *
|
||||
</label>
|
||||
<input
|
||||
id={amountId}
|
||||
name={amountId}
|
||||
type="text"
|
||||
required
|
||||
className="block w-full rounded-lg border p-3 py-2 shadow-sm outline-none placeholder:text-sm placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="How much are you willing to pay for this?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={roadmapCountId}
|
||||
className="mb-2 block text-sm font-semibold"
|
||||
>
|
||||
How many roadmaps you will be generating (daily, or monthly)? *
|
||||
</label>
|
||||
<textarea
|
||||
id={roadmapCountId}
|
||||
name={roadmapCountId}
|
||||
required
|
||||
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="How many roadmaps you will be generating (daily, or monthly)?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={usageId} className="mb-2 block text-sm font-semibold">
|
||||
How will you be using this feature? *
|
||||
</label>
|
||||
<textarea
|
||||
id={usageId}
|
||||
name={usageId}
|
||||
required
|
||||
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="How will you be using this"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={feedbackId}
|
||||
className="mb-2 block text-sm font-semibold"
|
||||
>
|
||||
Do you have any feedback for us to improve this feature?
|
||||
</label>
|
||||
<textarea
|
||||
id={feedbackId}
|
||||
name={feedbackId}
|
||||
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
|
||||
placeholder="Do you have any feedback?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="disbaled:opacity-60 w-full rounded-lg border border-gray-300 py-2 text-sm hover:bg-gray-100 disabled:cursor-not-allowed"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="disbaled:opacity-60 w-full rounded-lg bg-gray-900 py-2 text-sm text-white hover:bg-gray-800 disabled:cursor-not-allowed"
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { ChevronRight, ChevronUpIcon } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { increaseLimitTabs, type IncreaseTab } from './IncreaseRoadmapLimit';
|
||||
|
||||
type PickLimitOptionProps = {
|
||||
activeTab: IncreaseTab | null;
|
||||
setActiveTab: (tab: IncreaseTab | null) => void;
|
||||
};
|
||||
|
||||
export function PickLimitOption(props: PickLimitOptionProps) {
|
||||
const { activeTab, setActiveTab } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Generate more Roadmaps
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-700">
|
||||
Pick one of the options below to increase your roadmap limit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-1 px-3 pb-4">
|
||||
{increaseLimitTabs.map((tab) => {
|
||||
const isActive = tab.key === activeTab;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
setActiveTab(isActive ? null : tab.key);
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between gap-2 rounded-md border-t py-2 text-sm font-medium pl-3 pr-3',
|
||||
{
|
||||
'bg-gray-100 text-gray-800': isActive,
|
||||
'bg-gray-200 hover:bg-gray-300 transition-colors text-black': !isActive,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { Check, ChevronLeft, Clipboard } from 'lucide-react';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useRef } from 'react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type ReferYourFriendProps = {
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export function ReferYourFriend(props: ReferYourFriendProps) {
|
||||
const { onBack } = props;
|
||||
|
||||
const user = useAuth();
|
||||
const toast = useToast();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { copyText, isCopied } = useCopyText();
|
||||
const referralLink = new URL(
|
||||
`/ai?rc=${user?.id}`,
|
||||
import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh',
|
||||
).toString();
|
||||
|
||||
const handleCopy = () => {
|
||||
inputRef.current?.select();
|
||||
copyText(referralLink);
|
||||
toast.success('Copied to clipboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="mb-5 flex items-center gap-1.5 text-sm leading-none opacity-40 transition-opacity hover:opacity-100 focus:outline-none"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
Back to options
|
||||
</button>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Refer your Friends
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
Share the URL below with your friends. When they sign up with your link,
|
||||
you will get extra roadmap generation credits.
|
||||
</p>
|
||||
|
||||
<label className="mt-4 flex flex-col gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="w-full rounded-md border bg-gray-100 p-2 px-2.5 text-gray-700 focus:outline-none"
|
||||
value={referralLink}
|
||||
readOnly={true}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
|
||||
<button
|
||||
className={cn(
|
||||
'flex h-10 items-center justify-center gap-1.5 rounded-md p-2 px-2.5 text-sm',
|
||||
{
|
||||
'bg-green-500 text-black transition-colors': isCopied,
|
||||
'bg-black text-white rounded-md': !isCopied,
|
||||
},
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
disabled={isCopied}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Copied to Clipboard
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className="h-4 w-4" />
|
||||
Copy URL
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import { ArrowUpRight, Ban, Cog, Telescope, Wand } from 'lucide-react';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Ban,
|
||||
CircleFadingPlus,
|
||||
Cog,
|
||||
Telescope,
|
||||
Wand,
|
||||
} from 'lucide-react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getOpenAIKey, isLoggedIn } from '../../lib/jwt';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { useState } from 'react';
|
||||
import { OpenAISettings } from './OpenAISettings.tsx';
|
||||
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
|
||||
import { IncreaseRoadmapLimit } from './IncreaseRoadmapLimit.tsx';
|
||||
|
||||
type RoadmapSearchProps = {
|
||||
roadmapTerm: string;
|
||||
@@ -33,29 +38,22 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
|
||||
const canGenerateMore = limitUsed < limit;
|
||||
const [isConfiguring, setIsConfiguring] = useState(false);
|
||||
const [openAPIKey, setOpenAPIKey] = useState('');
|
||||
const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
|
||||
const [isLoadingResults, setIsLoadingResults] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setOpenAPIKey(getOpenAIKey() || '');
|
||||
setIsAuthenticatedUser(isLoggedIn());
|
||||
}, []);
|
||||
const openAPIKey = getOpenAIKey();
|
||||
const isAuthenticatedUser = isLoggedIn();
|
||||
|
||||
const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col items-center px-4 py-6 sm:px-6 md:my-24 lg:my-32">
|
||||
<div className="flex flex-grow flex-col items-center px-4 py-6 sm:px-6">
|
||||
{isConfiguring && (
|
||||
<IncreaseRoadmapLimit
|
||||
<OpenAISettings
|
||||
onClose={() => {
|
||||
setOpenAPIKey(getOpenAIKey()!);
|
||||
setIsConfiguring(false);
|
||||
loadAIRoadmapLimit();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-0 text-center sm:gap-2">
|
||||
<div className="flex flex-col gap-0 text-center sm:gap-2 md:mt-24 lg:mt-32">
|
||||
<h1 className="relative text-2xl font-medium sm:text-3xl">
|
||||
<span className="hidden sm:inline">Generate roadmaps with AI</span>
|
||||
<span className="inline sm:hidden">AI Roadmap Generator</span>
|
||||
@@ -80,15 +78,15 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
}}
|
||||
className="flex w-full flex-col gap-2 sm:flex-row"
|
||||
>
|
||||
<AITermSuggestionInput
|
||||
autoFocus={true}
|
||||
value={roadmapTerm}
|
||||
onValueChange={(value) => setRoadmapTerm(value)}
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="Enter a topic to generate a roadmap for"
|
||||
wrapperClassName="w-full"
|
||||
onSelect={(roadmapId, roadmapTitle) => {
|
||||
onLoadTerm(roadmapTitle);
|
||||
}}
|
||||
className="w-full rounded-md border border-gray-400 px-3 py-2.5 transition-colors focus:border-black focus:outline-none"
|
||||
value={roadmapTerm}
|
||||
onInput={(e) =>
|
||||
setRoadmapTerm((e.target as HTMLInputElement).value)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className={cn(
|
||||
@@ -102,44 +100,33 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
isLoadingResults ||
|
||||
(isAuthenticatedUser &&
|
||||
(!limit ||
|
||||
!roadmapTerm ||
|
||||
limitUsed >= limit ||
|
||||
(isKeyOnly && !openAPIKey)))
|
||||
isAuthenticatedUser &&
|
||||
(!limit ||
|
||||
!roadmapTerm ||
|
||||
limitUsed >= limit ||
|
||||
(isKeyOnly && !openAPIKey))
|
||||
}
|
||||
>
|
||||
{isLoadingResults && (
|
||||
{!isAuthenticatedUser && (
|
||||
<>
|
||||
<span>Please wait..</span>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoadingResults && (
|
||||
{isAuthenticatedUser && (
|
||||
<>
|
||||
{!isAuthenticatedUser && (
|
||||
{(!limit || canGenerateMore) && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
{isAuthenticatedUser && (
|
||||
<>
|
||||
{(!limit || canGenerateMore) && (
|
||||
<>
|
||||
<Wand size={20} />
|
||||
Generate
|
||||
</>
|
||||
)}
|
||||
|
||||
{limit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center text-base">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
{limit > 0 && !canGenerateMore && (
|
||||
<span className="flex items-center text-base">
|
||||
<Ban size={15} className="mr-2" />
|
||||
Limit reached
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -255,7 +242,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
>
|
||||
{limitUsed} of {limit}
|
||||
</span>{' '}
|
||||
roadmaps today.
|
||||
roadmaps.
|
||||
</p>
|
||||
{isAuthenticatedUser && (
|
||||
<p className="flex items-center text-sm">
|
||||
@@ -264,8 +251,10 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
|
||||
onClick={() => setIsConfiguring(true)}
|
||||
className="rounded-xl border border-current px-2 py-0.5 text-sm text-blue-500 transition-colors hover:bg-blue-400 hover:text-white"
|
||||
>
|
||||
Need to generate more?{' '}
|
||||
<span className="font-semibold">Click here.</span>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">
|
||||
adding your own OpenAI API key
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import {Ban, Cog, Contact, FileText, User, UserRound, X} from 'lucide-react';
|
||||
import { Ban, Cog, FileText, X } from 'lucide-react';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import type { RoadmapNodeDetails } from './GenerateRoadmap';
|
||||
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
|
||||
@@ -43,10 +43,12 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
|
||||
const generateAiRoadmapTopicContent = async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
// if (topicLimitUsed >= topicLimit) {
|
||||
// setError('Maximum limit reached');
|
||||
// setIsLoading(false);
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!roadmapId || !nodeTitle) {
|
||||
setIsLoading(false);
|
||||
@@ -131,44 +133,50 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
|
||||
tabIndex={0}
|
||||
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6"
|
||||
>
|
||||
{isLoggedIn() && (
|
||||
<div className="flex flex-col items-start gap-2 sm:flex-row">
|
||||
<span>
|
||||
<span
|
||||
className={cn(
|
||||
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
|
||||
{
|
||||
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||
!topicLimit,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{topicLimitUsed} of {topicLimit}
|
||||
</span>{' '}
|
||||
topics generated
|
||||
</span>
|
||||
{!openAIKey && (
|
||||
<button
|
||||
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={onConfigureOpenAI}
|
||||
>
|
||||
Need to generate more?{' '}
|
||||
<span className="font-semibold">Click here.</span>
|
||||
</button>
|
||||
)}
|
||||
{openAIKey && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={onConfigureOpenAI}
|
||||
>
|
||||
<Cog className="-mt-0.5 inline-block h-4 w-4" />
|
||||
Configure OpenAI Key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start gap-2 sm:flex-row">
|
||||
<span>
|
||||
<span
|
||||
className={cn(
|
||||
'mr-0.5 inline-block rounded-xl border px-1.5 text-center text-sm tabular-nums text-gray-800',
|
||||
{
|
||||
'animate-pulse border-zinc-300 bg-zinc-300 text-zinc-300':
|
||||
!topicLimit,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{topicLimitUsed} of {topicLimit}
|
||||
</span>{' '}
|
||||
topics generated
|
||||
</span>
|
||||
{!isLoggedIn() && (
|
||||
<button
|
||||
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={showLoginPopup}
|
||||
>
|
||||
Generate more by <span className="font-semibold">logging in</span>
|
||||
</button>
|
||||
)}
|
||||
{isLoggedIn() && !openAIKey && (
|
||||
<button
|
||||
className="rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={onConfigureOpenAI}
|
||||
>
|
||||
By-pass all limits by{' '}
|
||||
<span className="font-semibold">adding your own OpenAI Key</span>
|
||||
</button>
|
||||
)}
|
||||
{isLoggedIn() && openAIKey && (
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-xl border border-current px-1.5 py-0.5 text-left text-sm font-medium text-blue-500 sm:text-center"
|
||||
onClick={onConfigureOpenAI}
|
||||
>
|
||||
<Cog className="-mt-0.5 inline-block h-4 w-4" />
|
||||
Configure OpenAI Key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoggedIn() && isLoading && (
|
||||
{isLoading && (
|
||||
<div className="mt-6 flex w-full justify-center">
|
||||
<Spinner
|
||||
outerFill="#d1d5db"
|
||||
@@ -178,22 +186,6 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoggedIn() && (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<Contact className="h-14 w-14 text-gray-200 mb-3.5" />
|
||||
<h2 className='font-medium text-xl'>You must be logged in</h2>
|
||||
<p className="text-base text-gray-400">
|
||||
Sign up or login to generate topic content.
|
||||
</p>
|
||||
<button
|
||||
className="mt-3.5 text-base font-medium text-white bg-black px-3 py-2 rounded-md w-full max-w-[300px]"
|
||||
onClick={showLoginPopup}
|
||||
>
|
||||
Sign up / Login
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
|
||||
@@ -18,7 +18,7 @@ export function RoleRoadmaps(props: RoleRoadmapsProps) {
|
||||
<SectionBadge title={badge} />
|
||||
</div>
|
||||
<div className="my-4 sm:my-7 text-left">
|
||||
<h2 className="mb-1 text-balance text-xl sm:text-3xl font-semibold">{title}</h2>
|
||||
<h2 className="mb-1 text-xl sm:text-3xl font-semibold">{title}</h2>
|
||||
<p className="text-sm sm:text-base text-gray-500">{description}</p>
|
||||
|
||||
<div className="mt-4 sm:mt-7 grid sm:grid-cols-2 md:grid-cols-3 gap-3">{children}</div>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
import { getGuideTableOfContent, type GuideFileType } from '../../lib/guide';
|
||||
import MarkdownFile from '../MarkdownFile.astro';
|
||||
import { TableOfContent } from '../TableOfContent/TableOfContent';
|
||||
|
||||
interface Props {
|
||||
guide: GuideFileType;
|
||||
}
|
||||
|
||||
const { guide } = Astro.props;
|
||||
|
||||
const allHeadings = guide.getHeadings();
|
||||
const tableOfContent = getGuideTableOfContent(allHeadings);
|
||||
|
||||
const showTableOfContent = tableOfContent.length > 0;
|
||||
const { frontmatter: guideFrontmatter, author } = guide;
|
||||
---
|
||||
|
||||
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
|
||||
{
|
||||
showTableOfContent && (
|
||||
<div class='bg-gradient-to-r from-gray-50 py-0 lg:col-start-3 lg:col-end-4 lg:row-start-1'>
|
||||
<TableOfContent toc={tableOfContent} client:load />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div
|
||||
class:list={['col-start-2 col-end-3 row-start-1 mx-auto max-w-[700px] py-5 sm:py-10', {
|
||||
'lg:border-r': showTableOfContent
|
||||
}]}
|
||||
>
|
||||
<MarkdownFile>
|
||||
<h1 class='text-balance text-4xl mb-3 font-bold'>{guideFrontmatter.title}</h1>
|
||||
<p
|
||||
class='flex items-center justify-start text-sm text-gray-400 my-0'
|
||||
>
|
||||
<a
|
||||
href={`/authors/${author.id}`}
|
||||
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline underline-offset-2'
|
||||
>
|
||||
<img
|
||||
alt={author.frontmatter.name}
|
||||
src={author.frontmatter.imageUrl}
|
||||
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
|
||||
/>
|
||||
{author.frontmatter.name}
|
||||
</a>
|
||||
<span class='mx-2 hidden sm:inline'>·</span>
|
||||
<a
|
||||
class='hover:text-gray-600 underline-offset-2 hidden sm:inline'
|
||||
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`}
|
||||
target='_blank'
|
||||
>
|
||||
Improve this Guide
|
||||
</a>
|
||||
</p>
|
||||
<guide.Content />
|
||||
</MarkdownFile>
|
||||
</div>
|
||||
</article>
|
||||
@@ -7,8 +7,6 @@ export interface Props {
|
||||
|
||||
const { guide } = Astro.props;
|
||||
const { frontmatter, author } = guide;
|
||||
|
||||
return undefined;
|
||||
---
|
||||
|
||||
<div class='border-b bg-white py-5 sm:py-12'>
|
||||
@@ -42,7 +40,7 @@ return undefined;
|
||||
target='_blank'>Improve this Guide</a
|
||||
>
|
||||
</p>
|
||||
<h1 class='my-0 text-2xl font-bold sm:my-3.5 sm:text-5xl text-balance'>
|
||||
<h1 class='my-0 text-2xl font-bold sm:my-3.5 sm:text-5xl'>
|
||||
{frontmatter.title}
|
||||
</h1>
|
||||
<p class='hidden text-xl text-gray-400 sm:block'>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class='container prose-h2:text-balance prose-h3:text-balance prose-h4:text-balance prose-h5:text-balance prose prose-xl prose-h2:mb-3 prose-h2:mt-10 prose-h2:scroll-mt-5 prose-h2:text-3xl prose-h3:mt-2 prose-h3:scroll-mt-5 prose-h5:font-medium prose-blockquote:font-normal prose-code:bg-transparent prose-img:mt-1 prose-h2:sm:scroll-mt-10 prose-h3:sm:scroll-mt-10'
|
||||
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-10 prose-h2:mb-3 prose-h5:font-medium prose-h3:mt-2 prose-img:mt-1'
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -6,19 +6,12 @@ import { cn } from '../lib/classname';
|
||||
type ModalProps = {
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
overlayClassName?: string;
|
||||
bodyClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
};
|
||||
|
||||
export function Modal(props: ModalProps) {
|
||||
const {
|
||||
onClose,
|
||||
children,
|
||||
bodyClassName,
|
||||
wrapperClassName,
|
||||
overlayClassName,
|
||||
} = props;
|
||||
const { onClose, children, bodyClassName, wrapperClassName } = props;
|
||||
|
||||
const popupBodyEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -31,23 +24,18 @@ export function Modal(props: ModalProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'popup fixed left-0 right-0 top-0 z-[99] flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50',
|
||||
overlayClassName,
|
||||
)}
|
||||
>
|
||||
<div className="popup fixed left-0 right-0 top-0 z-[99] flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-full w-full max-w-md p-4 md:h-auto',
|
||||
wrapperClassName,
|
||||
wrapperClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={popupBodyEl}
|
||||
className={cn(
|
||||
'popup-body relative h-full rounded-lg bg-white shadow',
|
||||
bodyClassName,
|
||||
bodyClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -17,10 +17,12 @@ import { AccountDropdown } from './AccountDropdown';
|
||||
</a>
|
||||
|
||||
<a
|
||||
href='/ai'
|
||||
target='_blank'
|
||||
rel='noreferrer nofollow'
|
||||
href='https://boards.greenhouse.io/insightmediagroupllc/jobs/4002116008'
|
||||
class='group inline sm:hidden relative !mr-2 text-blue-300 hover:text-white'
|
||||
>
|
||||
AI Roadmaps
|
||||
We're Hiring
|
||||
|
||||
<span class='absolute -right-[11px] top-0'>
|
||||
<span class='relative flex h-2 w-2'>
|
||||
@@ -41,10 +43,12 @@ import { AccountDropdown } from './AccountDropdown';
|
||||
</a>
|
||||
<a href='/teams' class='text-gray-400 hover:text-white'> Teams</a>
|
||||
<a
|
||||
href='/ai'
|
||||
target='_blank'
|
||||
rel='noreferrer nofollow'
|
||||
href='https://boards.greenhouse.io/insightmediagroupllc/jobs/4002116008'
|
||||
class='group relative !mr-2 text-blue-300 hover:text-white'
|
||||
>
|
||||
AI Roadmaps
|
||||
We're Hiring
|
||||
|
||||
<span class='absolute -right-[11px] top-0'>
|
||||
<span class='relative flex h-2 w-2'>
|
||||
|
||||
@@ -73,9 +73,9 @@ export function NavigationDropdown() {
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute pointer-events-none invisible left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
'absolute pointer-events-none left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
|
||||
{
|
||||
'pointer-events-auto visible translate-y-2.5 opacity-100': isOpen,
|
||||
'pointer-events-auto translate-y-2.5 opacity-100': isOpen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ const discordInfo = await getDiscordInfo();
|
||||
</p>
|
||||
|
||||
<div
|
||||
class='mt-5 grid grid-cols-1 justify-between gap-2 divide-x-0 sm:my-11 sm:grid-cols-3 sm:gap-0 sm:divide-x mb-4 sm:mb-0'
|
||||
class='mt-5 grid grid-cols-1 justify-between gap-2 divide-x-0 sm:my-11 sm:grid-cols-3 sm:gap-0 sm:divide-x'
|
||||
>
|
||||
<OpenSourceStat text='GitHub Stars' value={starCount} />
|
||||
<OpenSourceStat text='Registered Users' value={'850k'} />
|
||||
|
||||
@@ -14,7 +14,7 @@ const isDiscordMembers = text.toLowerCase() === 'discord members';
|
||||
---
|
||||
|
||||
<div
|
||||
class='flex items-start sm:items-center justify-start flex-col sm:justify-center sm:gap-0 gap-2 sm:bg-transparent sm:rounded-none rounded-xl p-0 sm:p-4 mb-3 sm:mb-0'
|
||||
class='flex items-start sm:items-center justify-start flex-col sm:justify-center sm:gap-0 gap-2 sm:bg-transparent bg-gray-200 sm:rounded-none rounded-xl p-4'
|
||||
>
|
||||
{
|
||||
isGitHubStars && (
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { usePagination } from '../../hooks/use-pagination.ts';
|
||||
import { MoreHorizontal } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { formatCommaNumber } from '../../lib/number.ts';
|
||||
|
||||
type PaginationProps = {
|
||||
variant?: 'minimal' | 'default';
|
||||
totalPages: number;
|
||||
currPage: number;
|
||||
perPage: number;
|
||||
totalCount: number;
|
||||
isDisabled?: boolean;
|
||||
onPageChange: (page: number) => void;
|
||||
};
|
||||
|
||||
export function Pagination(props: PaginationProps) {
|
||||
const {
|
||||
variant = 'default',
|
||||
onPageChange,
|
||||
totalCount,
|
||||
totalPages,
|
||||
currPage,
|
||||
perPage,
|
||||
isDisabled = false,
|
||||
} = props;
|
||||
|
||||
if (!totalPages || totalPages === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pages = usePagination(currPage, totalPages, 5);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex items-center', {
|
||||
'justify-between': variant === 'default',
|
||||
'justify-start': variant === 'minimal',
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center gap-1 text-xs font-medium">
|
||||
<button
|
||||
onClick={() => {
|
||||
onPageChange(currPage - 1);
|
||||
}}
|
||||
disabled={currPage === 1 || isDisabled}
|
||||
className="rounded-md border px-2 py-1 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
{variant === 'default' && (
|
||||
<>
|
||||
{pages.map((page, counter) => {
|
||||
if (page === 'more') {
|
||||
return (
|
||||
<span
|
||||
key={`page-${page}-${counter}`}
|
||||
className="hidden sm:block"
|
||||
>
|
||||
<MoreHorizontal className="text-gray-400" size={14} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`page-${page}`}
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
onPageChange(page as number);
|
||||
}}
|
||||
className={cn(
|
||||
'hidden rounded-md border px-2 py-1 hover:bg-gray-100 sm:block',
|
||||
{
|
||||
'border-black text-black': currPage === page,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
disabled={currPage === totalPages || isDisabled}
|
||||
className="rounded-md border px-2 py-1 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
onClick={() => {
|
||||
onPageChange(currPage + 1);
|
||||
}}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
<span className="ml-2 hidden text-sm font-normal text-gray-500 sm:block">
|
||||
Showing {formatCommaNumber((currPage - 1) * perPage)} to{' '}
|
||||
{formatCommaNumber((currPage - 1) * perPage + perPage)} of{' '}
|
||||
{formatCommaNumber(totalCount)} entries
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { UpdateEmailForm } from '../UpdateEmail/UpdateEmailForm';
|
||||
import UpdatePasswordForm from '../UpdatePassword/UpdatePasswordForm';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
|
||||
export function ProfileSettingsPage() {
|
||||
const toast = useToast();
|
||||
|
||||
const [authProvider, setAuthProvider] = useState('');
|
||||
const [currentEmail, setCurrentEmail] = useState('');
|
||||
const [newEmail, setNewEmail] = useState('');
|
||||
|
||||
const loadProfile = async () => {
|
||||
const { error, response } = await httpGet(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-me`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { authProvider, email, newEmail } = response;
|
||||
setAuthProvider(authProvider);
|
||||
setCurrentEmail(email);
|
||||
setNewEmail(newEmail || '');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdatePasswordForm authProvider={authProvider} />
|
||||
<hr className="my-8" />
|
||||
<UpdateEmailForm
|
||||
authProvider={authProvider}
|
||||
currentEmail={currentEmail}
|
||||
newEmail={newEmail}
|
||||
key={newEmail}
|
||||
onSendVerificationCode={(newEmail) => {
|
||||
setNewEmail(newEmail);
|
||||
loadProfile().finally(() => {});
|
||||
}}
|
||||
onVerificationCancel={() => {
|
||||
loadProfile().finally(() => {});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type CategoryFilterButtonProps = {
|
||||
category: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export function CategoryFilterButton(props: CategoryFilterButtonProps) {
|
||||
const { category, selected, onClick } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'border-b bg-gradient-to-l py-1.5 pr-3 text-center text-sm text-gray-500 hover:text-gray-900 sm:text-right',
|
||||
{
|
||||
'from-white font-semibold text-gray-900':
|
||||
selected && category !== 'All Roadmaps',
|
||||
'font-semibold text-gray-900':
|
||||
selected && category === 'All Roadmaps',
|
||||
'hover:from-white': category !== 'All Roadmaps',
|
||||
},
|
||||
)}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { Filter, X } from 'lucide-react';
|
||||
import { CategoryFilterButton } from './CategoryFilterButton.tsx';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click.ts';
|
||||
|
||||
const groupNames = [
|
||||
'Absolute Beginners',
|
||||
'Web Development',
|
||||
'Languages / Platforms',
|
||||
'Frameworks',
|
||||
'Mobile Development',
|
||||
'Databases',
|
||||
'Computer Science',
|
||||
'Machine Learning',
|
||||
'Game Development',
|
||||
'Design',
|
||||
'DevOps',
|
||||
'Blockchain',
|
||||
'Cyber Security',
|
||||
];
|
||||
|
||||
type AllowGroupNames = (typeof groupNames)[number];
|
||||
|
||||
type GroupType = {
|
||||
group: AllowGroupNames;
|
||||
roadmaps: {
|
||||
title: string;
|
||||
link: string;
|
||||
type: 'role' | 'skill';
|
||||
otherGroups?: AllowGroupNames[];
|
||||
}[];
|
||||
};
|
||||
|
||||
const groups: GroupType[] = [
|
||||
{
|
||||
group: 'Absolute Beginners',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Frontend Beginner',
|
||||
link: '/frontend?r=frontend-beginner',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Backend Beginner',
|
||||
link: '/backend?r=backend-beginner',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'DevOps Beginner',
|
||||
link: '/devops?r=devops-beginner',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Web Development',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Frontend',
|
||||
link: '/frontend',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Backend',
|
||||
link: '/backend',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Full Stack',
|
||||
link: '/full-stack',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development', 'Absolute Beginners'],
|
||||
},
|
||||
{
|
||||
title: 'QA',
|
||||
link: '/qa',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'GraphQL',
|
||||
link: '/graphql',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Frameworks',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'React',
|
||||
link: '/react',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Vue',
|
||||
link: '/vue',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Angular',
|
||||
link: '/angular',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Spring Boot',
|
||||
link: '/spring-boot',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'ASP.NET Core',
|
||||
link: '/aspnet-core',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Languages / Platforms',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'JavaScript',
|
||||
link: '/javascript',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'DevOps', 'Mobile Development'],
|
||||
},
|
||||
{
|
||||
title: 'TypeScript',
|
||||
link: '/typescript',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'Mobile Development'],
|
||||
},
|
||||
{
|
||||
title: 'Node.js',
|
||||
link: '/nodejs',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'DevOps'],
|
||||
},
|
||||
{
|
||||
title: 'C++',
|
||||
link: '/cpp',
|
||||
type: 'skill',
|
||||
},
|
||||
{
|
||||
title: 'Go',
|
||||
link: '/golang',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'DevOps'],
|
||||
},
|
||||
{
|
||||
title: 'Rust',
|
||||
link: '/rust',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'DevOps'],
|
||||
},
|
||||
{
|
||||
title: 'Python',
|
||||
link: '/python',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'DevOps'],
|
||||
},
|
||||
{
|
||||
title: 'Java',
|
||||
link: '/java',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'SQL',
|
||||
link: '/sql',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development', 'Databases', 'DevOps'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'DevOps',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'DevOps',
|
||||
link: '/devops',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Docker',
|
||||
link: '/docker',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Kubernetes',
|
||||
link: '/kubernetes',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'AWS',
|
||||
link: '/aws',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Mobile Development',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Android',
|
||||
link: '/android',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'React Native',
|
||||
link: '/react-native',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Flutter',
|
||||
link: '/flutter',
|
||||
type: 'role',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Databases',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'PostgreSQL',
|
||||
link: '/postgresql-dba',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'MongoDB',
|
||||
link: '/mongodb',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Computer Science',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Computer Science',
|
||||
link: '/computer-science',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Data Structures',
|
||||
link: '/datastructures-and-algorithms',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'System Design',
|
||||
link: '/system-design',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Design and Architecture',
|
||||
link: '/software-design-architecture',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Software Architect',
|
||||
link: '/software-architect',
|
||||
type: 'role',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Code Review',
|
||||
link: '/code-review',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
{
|
||||
title: 'Technical Writer',
|
||||
link: '/technical-writer',
|
||||
type: 'role',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Machine Learning',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'AI and Data Scientist',
|
||||
link: '/ai-data-scientist',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Data Analyst',
|
||||
link: '/data-analyst',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'MLOps',
|
||||
link: '/mlops',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Prompt Engineering',
|
||||
link: '/prompt-engineering',
|
||||
type: 'skill',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Game Development',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Client Side Game Dev.',
|
||||
link: '/game-developer',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Server Side Game Dev.',
|
||||
link: '/server-side-game-developer',
|
||||
type: 'role',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Design',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'UX Design',
|
||||
link: '/ux-design',
|
||||
type: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Design System',
|
||||
link: '/design-system',
|
||||
type: 'skill',
|
||||
otherGroups: ['Web Development'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Blockchain',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Blockchain',
|
||||
link: '/blockchain',
|
||||
type: 'role',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Cyber Security',
|
||||
roadmaps: [
|
||||
{
|
||||
title: 'Cyber Security',
|
||||
link: '/cyber-security',
|
||||
type: 'role',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const roleRoadmaps = groups.flatMap((group) =>
|
||||
group.roadmaps.filter((roadmap) => roadmap.type === 'role'),
|
||||
);
|
||||
const skillRoadmaps = groups.flatMap((group) =>
|
||||
group.roadmaps.filter((roadmap) => roadmap.type === 'skill'),
|
||||
);
|
||||
|
||||
const allGroups = [
|
||||
{
|
||||
group: 'Role Based Roadmaps',
|
||||
roadmaps: roleRoadmaps,
|
||||
},
|
||||
{
|
||||
group: 'Skill Based Roadmaps',
|
||||
roadmaps: skillRoadmaps,
|
||||
},
|
||||
];
|
||||
|
||||
export function RoadmapsPage() {
|
||||
const [activeGroup, setActiveGroup] = useState<AllowGroupNames>('');
|
||||
const [visibleGroups, setVisibleGroups] = useState<GroupType[]>(allGroups);
|
||||
|
||||
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeGroup) {
|
||||
setVisibleGroups(allGroups);
|
||||
return;
|
||||
}
|
||||
|
||||
const group = groups.find((group) => group.group === activeGroup);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
// other groups that have a roadmap that is in the same group
|
||||
const otherGroups = groups.filter((g) => {
|
||||
return (
|
||||
g.group !== group.group &&
|
||||
g.roadmaps.some((roadmap) => {
|
||||
return roadmap.otherGroups?.includes(group.group);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
setVisibleGroups([
|
||||
group,
|
||||
...otherGroups.map((g) => ({
|
||||
...g,
|
||||
roadmaps: g.roadmaps.filter((roadmap) =>
|
||||
roadmap.otherGroups?.includes(group.group),
|
||||
),
|
||||
})),
|
||||
]);
|
||||
}, [activeGroup]);
|
||||
|
||||
return (
|
||||
<div className="border-t bg-gray-100">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsFilterOpen(!isFilterOpen);
|
||||
}}
|
||||
id="filter-button"
|
||||
className={cn(
|
||||
'-mt-1 flex w-full items-center justify-center bg-gray-300 py-2 text-sm text-black focus:shadow-none focus:outline-0 sm:hidden',
|
||||
{
|
||||
'mb-3': !isFilterOpen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{!isFilterOpen && <Filter size={13} className="mr-1" />}
|
||||
{isFilterOpen && <X size={13} className="mr-1" />}
|
||||
Categories
|
||||
</button>
|
||||
<div className="container relative flex flex-col gap-4 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
'hidden w-full flex-col from-gray-100 sm:w-[180px] sm:border-r sm:bg-gradient-to-l sm:pt-6',
|
||||
{
|
||||
'hidden sm:flex': !isFilterOpen,
|
||||
'z-50 flex': isFilterOpen,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 -mx-4 w-full bg-white pb-0 shadow-xl sm:sticky sm:top-10 sm:mx-0 sm:bg-transparent sm:pb-20 sm:shadow-none">
|
||||
<div className="grid grid-cols-1">
|
||||
<CategoryFilterButton
|
||||
onClick={() => {
|
||||
setActiveGroup('');
|
||||
setIsFilterOpen(false);
|
||||
}}
|
||||
category={'All Roadmaps'}
|
||||
selected={activeGroup === ''}
|
||||
/>
|
||||
|
||||
{groups.map((group) => (
|
||||
<CategoryFilterButton
|
||||
key={group.group}
|
||||
onClick={() => {
|
||||
setActiveGroup(group.group);
|
||||
setIsFilterOpen(false);
|
||||
document?.getElementById('filter-button')?.scrollIntoView();
|
||||
}}
|
||||
category={group.group}
|
||||
selected={activeGroup === group.group}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-6 pb-20 pt-2 sm:pt-8">
|
||||
{visibleGroups.map((group) => (
|
||||
<div key={`${group.group}-${group.roadmaps.length}`}>
|
||||
<h2 className="mb-2 text-xs uppercase tracking-wide text-gray-400">
|
||||
{group.group}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2 md:grid-cols-3">
|
||||
{group.roadmaps.map((roadmap) => (
|
||||
<a
|
||||
key={roadmap.link}
|
||||
className="rounded-md border bg-white px-3 py-2 text-left text-sm shadow-sm transition-all hover:border-gray-300 hover:bg-gray-50"
|
||||
href={roadmap.link}
|
||||
>
|
||||
{roadmap.title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { isLoggedIn } from '../../lib/jwt.ts';
|
||||
import { showLoginPopup } from '../../lib/popup.ts';
|
||||
|
||||
export function RoadmapsPageHeader() {
|
||||
return (
|
||||
<div className="bg-white py-3 sm:py-12">
|
||||
<div className="container">
|
||||
<div className="flex flex-col items-start bg-white sm:items-center">
|
||||
<h1 className="text-2xl font-bold sm:text-5xl">Developer Roadmaps</h1>
|
||||
<p className="mb-3 mt-1 text-sm sm:my-3 sm:text-lg">
|
||||
Browse the ever-growing list of up-to-date, community driven
|
||||
roadmaps
|
||||
</p>
|
||||
<p className="mb-3 flex w-full flex-col gap-1.5 sm:mb-0 sm:w-auto sm:flex-row sm:gap-3">
|
||||
<a
|
||||
className="inline-block rounded-md bg-black px-3.5 py-2 text-sm text-white sm:py-1.5 sm:text-base"
|
||||
href="https://draw.roadmap.sh"
|
||||
onClick={(e) => {
|
||||
if (!isLoggedIn()) {
|
||||
e.preventDefault();
|
||||
showLoginPopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Draw your own roadmap
|
||||
</a>
|
||||
<a
|
||||
className="inline-block rounded-md bg-gray-300 px-3.5 py-2 text-sm text-black sm:py-1.5 sm:text-base"
|
||||
href="https://roadmap.sh/ai"
|
||||
onClick={(e) => {
|
||||
if (!isLoggedIn()) {
|
||||
e.preventDefault();
|
||||
showLoginPopup();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Generate Roadmaps with AI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { useState, type CSSProperties } from 'react';
|
||||
import type { HeadingGroupType } from '../../lib/guide';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type TableOfContentProps = {
|
||||
toc: HeadingGroupType[];
|
||||
};
|
||||
|
||||
export function TableOfContent(props: TableOfContentProps) {
|
||||
const { toc } = props;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (toc.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalRows = toc.flatMap((heading) => {
|
||||
return [heading, ...heading.children];
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative min-w-[250px] px-5 pt-0 max-lg:min-w-full max-lg:max-w-full max-lg:border-none max-lg:px-0 lg:pt-10',
|
||||
{
|
||||
'top-0 lg:!sticky': totalRows <= 20,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<h4 className="text-lg font-medium max-lg:hidden">In this article</h4>
|
||||
<button
|
||||
className="flex w-full items-center justify-between gap-2 bg-gray-300 px-3 py-2 text-sm font-medium lg:hidden"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Table of Contents
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={cn(
|
||||
'transform transition-transform',
|
||||
isOpen && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<ol
|
||||
className={cn(
|
||||
'mt-0.5 max-lg:absolute max-lg:top-full max-lg:mt-0 max-lg:w-full space-y-0 max-lg:bg-white max-lg:shadow',
|
||||
!isOpen && 'hidden lg:block',
|
||||
isOpen && 'block',
|
||||
)}
|
||||
>
|
||||
{toc.map((heading) => (
|
||||
<li key={heading.slug}>
|
||||
<a
|
||||
href={`#${heading.slug}`}
|
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1"
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
|
||||
{heading.children.length > 0 && (
|
||||
<ol className="my-0 ml-4 mt-1 max-lg:ml-0 max-lg:mt-0 max-lg:list-none space-y-0">
|
||||
{heading.children.map((children) => {
|
||||
return (
|
||||
<li key={children.slug}>
|
||||
<a
|
||||
href={`#${children.slug}`}
|
||||
className="text-sm text-gray-500 no-underline hover:text-black max-lg:block max-lg:border-b max-lg:px-3 max-lg:py-1 max-lg:pl-8"
|
||||
onClick={() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{children.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { httpPatch } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { ArrowUpRight, X } from 'lucide-react';
|
||||
|
||||
type UpdateEmailFormProps = {
|
||||
authProvider: string;
|
||||
currentEmail: string;
|
||||
newEmail?: string;
|
||||
onSendVerificationCode?: (newEmail: string) => void;
|
||||
onVerificationCancel?: () => void;
|
||||
};
|
||||
|
||||
export function UpdateEmailForm(props: UpdateEmailFormProps) {
|
||||
const {
|
||||
authProvider,
|
||||
currentEmail,
|
||||
newEmail: defaultNewEmail = '',
|
||||
onSendVerificationCode,
|
||||
onVerificationCancel,
|
||||
} = props;
|
||||
const toast = useToast();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitted, setIsSubmitted] = useState(defaultNewEmail !== '');
|
||||
const [newEmail, setNewEmail] = useState(defaultNewEmail);
|
||||
const [isResendDone, setIsResendDone] = useState(false);
|
||||
|
||||
const handleSentVerificationCode = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!newEmail || !newEmail.includes('@') || isSubmitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
pageProgressMessage.set('Sending verification code');
|
||||
const { response, error } = await httpPatch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-update-user-email`,
|
||||
{ email: newEmail },
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
setIsLoading(false);
|
||||
pageProgressMessage.set('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pageProgressMessage.set('');
|
||||
setIsLoading(false);
|
||||
setIsSubmitted(true);
|
||||
onSendVerificationCode?.(newEmail);
|
||||
};
|
||||
|
||||
const handleResendVerificationCode = async () => {
|
||||
if (isResendDone) {
|
||||
toast.error('You have already resent the verification code');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
pageProgressMessage.set('Resending verification code');
|
||||
const { response, error } = await httpPatch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-resend-email-verification-code`,
|
||||
{ email: newEmail },
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
setIsLoading(false);
|
||||
pageProgressMessage.set('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('Verification code has been resent');
|
||||
pageProgressMessage.set('');
|
||||
setIsResendDone(true);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handleCancelEmailVerification = async () => {
|
||||
setIsLoading(true);
|
||||
pageProgressMessage.set('Cancelling email verification');
|
||||
const { response, error } = await httpPatch(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-cancel-email-verification`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
setIsLoading(false);
|
||||
pageProgressMessage.set('');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pageProgressMessage.set('');
|
||||
onVerificationCancel?.();
|
||||
setIsSubmitted(false);
|
||||
setNewEmail('');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
if (authProvider && authProvider !== 'email') {
|
||||
return (
|
||||
<div className="block">
|
||||
<h2 className="text-xl font-bold sm:text-2xl">Update Email</h2>
|
||||
<p className="mt-2 text-gray-400">
|
||||
You have used {authProvider} when signing up. Please set your password
|
||||
first.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex w-full flex-col">
|
||||
<label
|
||||
htmlFor="current-email"
|
||||
className="text-sm leading-none text-slate-500"
|
||||
>
|
||||
Current Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="current-email"
|
||||
id="current-email"
|
||||
autoComplete="current-email"
|
||||
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"
|
||||
required
|
||||
disabled
|
||||
value={currentEmail}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 rounded-lg border border-red-600 px-2 py-1 text-red-600">
|
||||
Please set your password first to update your email.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-8 block">
|
||||
<h2 className="text-xl font-bold sm:text-2xl">Update Email</h2>
|
||||
<p className="mt-2 text-gray-400">
|
||||
Use the form below to update your email.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSentVerificationCode} className="space-y-4">
|
||||
<div className="flex w-full flex-col">
|
||||
<label
|
||||
htmlFor="current-email"
|
||||
className="text-sm leading-none text-slate-500"
|
||||
>
|
||||
Current Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="current-email"
|
||||
id="current-email"
|
||||
autoComplete="current-email"
|
||||
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"
|
||||
required
|
||||
disabled
|
||||
value={currentEmail}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn('flex w-full flex-col', {
|
||||
'rounded-lg border border-green-500 p-3': isSubmitted,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<label
|
||||
htmlFor="new-email"
|
||||
className="text-sm leading-none text-slate-500"
|
||||
>
|
||||
New Email
|
||||
</label>
|
||||
|
||||
{isSubmitted && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResendVerificationCode}
|
||||
disabled={isLoading || isResendDone}
|
||||
className="flex items-center gap-1 text-sm font-medium leading-none text-green-600 transition-colors hover:text-green-700"
|
||||
>
|
||||
<span className="hidden sm:block">
|
||||
Resend Verification Link
|
||||
</span>
|
||||
<span className="sm:hidden">Resend Code</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
name="new-email"
|
||||
id="new-email"
|
||||
autoComplete={'new-email'}
|
||||
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"
|
||||
required
|
||||
placeholder="Enter new email"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
disabled={isSubmitted}
|
||||
/>
|
||||
{!isSubmitted && (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
isLoading || !newEmail || !newEmail.includes('@') || isSubmitted
|
||||
}
|
||||
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"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Send Verification Link'}
|
||||
</button>
|
||||
)}
|
||||
{isSubmitted && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelEmailVerification}
|
||||
disabled={isLoading}
|
||||
className="font-regular mt-4 w-full rounded-lg border border-red-600 py-2 text-sm text-red-600 outline-none transition-colors hover:bg-red-500 hover:text-white focus:ring-2 focus:ring-red-500 focus:ring-offset-1"
|
||||
>
|
||||
Cancel Update
|
||||
</button>
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg bg-green-100 p-4">
|
||||
<span className="text-sm text-green-800">
|
||||
A verification link has been sent to your{' '}
|
||||
<span>new email address</span>. Please follow the instructions
|
||||
in email to verify and update your email.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,26 @@
|
||||
import { type FormEvent, useState } from 'react';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
|
||||
type UpdatePasswordFormProps = {
|
||||
authProvider: string;
|
||||
};
|
||||
|
||||
export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
const { authProvider } = props;
|
||||
|
||||
const toast = useToast();
|
||||
import { type FormEvent, useEffect, useState } from 'react';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
|
||||
export default function UpdatePasswordForm() {
|
||||
const [authProvider, setAuthProvider] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
toast.error('Passwords do not match');
|
||||
setError('Passwords do not match');
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
@@ -34,26 +32,50 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth',
|
||||
password: newPassword,
|
||||
confirmPassword: newPasswordConfirmation,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
if (error) {
|
||||
setError(error.message || 'Something went wrong');
|
||||
setIsLoading(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setError('');
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setNewPasswordConfirmation('');
|
||||
toast.success('Password updated successfully');
|
||||
setSuccess('Password updated successfully');
|
||||
setIsLoading(false);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const loadProfile = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const { error, response } = await httpGet(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-me`
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsLoading(false);
|
||||
setError(error?.message || 'Something went wrong');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { authProvider } = response;
|
||||
setAuthProvider(authProvider);
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile().finally(() => {
|
||||
pageProgressMessage.set('');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-8 hidden md:block">
|
||||
@@ -76,7 +98,7 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
type="password"
|
||||
name="current-password"
|
||||
id="current-password"
|
||||
autoComplete={'current-password'}
|
||||
autoComplete={"current-password"}
|
||||
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 disabled:bg-gray-100"
|
||||
required
|
||||
minLength={6}
|
||||
@@ -100,7 +122,7 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
type="password"
|
||||
name="new-password"
|
||||
id="new-password"
|
||||
autoComplete={'new-password'}
|
||||
autoComplete={"new-password"}
|
||||
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"
|
||||
required
|
||||
minLength={6}
|
||||
@@ -123,7 +145,7 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
name="new-password-confirmation"
|
||||
id="new-password-confirmation"
|
||||
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"
|
||||
autoComplete={'new-password'}
|
||||
autoComplete={"new-password"}
|
||||
required
|
||||
minLength={6}
|
||||
placeholder="Confirm New Password"
|
||||
@@ -134,11 +156,19 @@ export default function UpdatePasswordForm(props: UpdatePasswordFormProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<p className="mt-2 rounded-lg bg-green-100 p-2 text-green-700">
|
||||
{success}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
isLoading || !newPassword || newPassword !== newPasswordConfirmation
|
||||
}
|
||||
disabled={isLoading}
|
||||
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"
|
||||
>
|
||||
{isLoading ? 'Please wait...' : 'Update Password'}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function UpdateProfileForm() {
|
||||
linkedin: linkedin || undefined,
|
||||
twitter: twitter || undefined,
|
||||
website: website || undefined,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -45,10 +45,11 @@ export function UpdateProfileForm() {
|
||||
};
|
||||
|
||||
const loadProfile = async () => {
|
||||
// Set the loading state
|
||||
setIsLoading(true);
|
||||
|
||||
const { error, response } = await httpGet(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-me`,
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-me`
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
jsonUrl: '/jsons/best-practices/backend-performance.json'
|
||||
pdfUrl: '/pdfs/best-practices/backend-performance.pdf'
|
||||
order: 1
|
||||
briefTitle: 'Backend Performance'
|
||||
briefDescription: 'Backend Performance Best Practices'
|
||||
isNew: true
|
||||
isUpcoming: false
|
||||
title: 'Backend Performance Best Practices'
|
||||
description: 'Detailed list of best practices to improve your backend performance'
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1789.23
|
||||
schema:
|
||||
headline: 'Backend Performance Best Practices'
|
||||
description: 'Detailed list of best practices to improve the backend performance of your website. Each best practice carries further details and how to implement that best practice.'
|
||||
imageUrl: 'https://roadmap.sh/best-practices/backend-performance.png'
|
||||
datePublished: '2023-01-23'
|
||||
dateModified: '2023-01-23'
|
||||
seo:
|
||||
title: 'Backend Performance Best Practices'
|
||||
description: 'Detailed list of best practices to improve the backend performance of your website. Each best practice carries further details and how to implement that best practice.'
|
||||
keywords:
|
||||
- 'backend performance'
|
||||
- 'api performance'
|
||||
- 'backend performance best practices'
|
||||
- 'backend performance checklist'
|
||||
- 'backend checklist'
|
||||
- 'make performant backends'
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
# Architectural Styles and Service Decomposition
|
||||
|
||||
Backend performance in web applications greatly depends on the selection of architectural styles like Service-Oriented Architecture (SOA) or Microservices and the ability to decompose services when necessary. For instance, using Microservices, an application is broken into smaller, loosely coupled services, making it easy to maintain and scale, improving the overall backend performance. Service decomposition, on the other hand, allows for the distribution of responsibilities, meaning if one service fails, it won't likely impact the entire system. Thus, understanding and efficiently managing architectural styles and service decomposition are critical for the optimized backend performance in web applications.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Asynchronous Logging Mechanisms
|
||||
|
||||
To optimise backend performance in web applications, implementing asynchronous logging mechanisms becomes crucial. It diminishes the logging overhead, thereby speeding up the execution flow of an application. For instance, the application does not need to wait for the logging data to be written on the disk, as the writing task is executed in the background, enabling the next instructions to execute without interruption. This also prevents unnecessary queuing of tasks, thereby bolstering the overall throughput of the backend operations. Netflix's open-source tool called 'Zuul' exhibits this concept where they use async logging to achieve scalability in high traffic.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Implementing Proper Authentication and Authorization
|
||||
|
||||
In the backend performance of web applications, robust authentication and authorization play an integral role. Having tight security measures ensures the application's optimal functioning by preventing unauthorized access. These precautionary measures protect the system from external threats such as data breaches or malicious attacks. For example, imagine a banking application without stringent authentication procedures. It could be easily exploited by hackers, leading to serious loss of finances and damage to the bank's reputation. Therefore, secure authentication and authorization is essential for maintaining the application's integrity and stability, ultimately contributing to efficient backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Efficient Cache-Invalidation Strategies
|
||||
|
||||
In the realm of backend performance, adopting proper cache-invalidation strategies is highly relevant. Effective cache management takes the pressure off web servers by saving and displaying previously retrieved or computed data. However, the challenge arises when such cached data becomes outdated, or 'stale'. If not addressed, users may be presented incorrect or obsolete information. Good cache-invalidation strategies ensure that the system constantly refreshes or dumps outdated cache, keeping the data consistent and accurate. For example, using time-based strategies, a system could invalidate cache after a set period, essentially creating a self-maintenance regimen. Similarly, with a write-through approach, an application updates the cache immediately as changes are made, guaranteeing the users always receive the most recent data.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Implementing Caching at Various Levels
|
||||
|
||||
In web development, backend performance significantly depends on the speed at which data is fetched and delivered. Implementing caching at various levels like database query results, HTML fragments, or even full-page, boosts the efficiency of data retrieval processes. Through caching, redundant data fetching is avoided leading to faster response times and reduced server load. For instance, when a database query result is cached, the system doesn't have to run the same operation repetitively thus enhancing speed. Moreover, in HTML fragments caching, reusable parts of a web page get stored, so they don't have to be reprocessed for every request, improving load times. Full-page caching, on the other hand, saves a rendered copy of the whole page, offering immediate response upon user's request. Each of these cache implementations enhances performance, increases scalability and improves user experience in web applications.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Application of Suitable Caching Patterns
|
||||
|
||||
For optimal backend performance in web applications, implementing the correct caching approach, such as cache aside, write-through, or read-through caching, matters greatly. This is significant fundamentally because it reduces the load on your database, fetching data quicker and decreasing the latency time, leading to faster response times. For instance, consider a high-traffic e-commerce site where hundreds of thousands of product details need to be fetched simultaneously. If a suitable caching pattern like the read-through cache is applied here, it would handle retrieving data from the database when the cache is empty, ensuring that the application always receives data, improving the overall performance and user experience.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utilization of CDNs for Static and Frequently Accessed Assets
|
||||
|
||||
For optimal backend performance in web applications, the use of Content Delivery Networks (CDNs) for serving static and frequently accessed assets is paramount. CDNs enhance website loading speed by storing a cached version of its content in multiple geographical locations. As such, when a user requests a website, the content is delivered from the nearest server, dramatically reducing latency and packet loss. This is especially beneficial for static and frequently accessed assets that remain unchanged over time like CSS, JavaScript files or Image files. For instance, a user in London trying to access a US-based web application can retrieve static content from a closer server in the UK rather than crossing the Atlantic every time, ensuring efficient and speedy content delivery.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Regular Maintenance and Cleanup of Data
|
||||
|
||||
Optimizing the backend performance in web applications depends greatly on how data is managed. Unnecessary or unused data could cause the system to slow down, impacting the efficiency of the backend processes. Regular cleanup of such data ensures that the server is not overburdened, allowing faster retrieval and storage of information. Similarly, routine database maintenance tasks like vacuuming and indexing help boost performance. Vacuuming helps remove stale or obsolete data, freeing up space and preventing system delays. Indexing, on the other hand, organizes data in a way that makes it easily retrievable, speeding up query response times. It's like using a well-organized filing system rather than a jumbled heap of papers. Additionally, optimizing queries aids in reducing the time taken for database interactions. An example of this would be replacing a nested query with a join, thereby reducing the processing time. Altogether, these practices lead to improved backend performance, ensuring smooth and efficient functioning of web applications.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utilizing Compiled Languages like Go or Rust
|
||||
|
||||
The backend performance of web applications can be substantially augmented by incorporating compiled languages such as Go or Rust. The essence of this lies in the manner these languages handle the conversion of code into machine language. Unlike interpreted languages, which convert the code into machine language during runtime, compiled languages do this step beforehand. This increased efficiency in translation results in faster performance of the code, especially valuable for performance-critical segments of your backend. For instance, Google uses Go language in several of their production systems for the very reason of increased performance and scalability. Similarly, Rust has gained acclaim in building highly concurrent and fast systems. Thus, using such compiled languages can greatly boost the overall backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimizing Connection Pool Settings
|
||||
|
||||
Backend performance of web applications can be significantly improved by fine-tuning connection pool settings. One of the main causes of performance degradation is the unnecessary utilization of resources. If a web application can reuse existing connections (via connection reuse parameters), instead of creating new ones each time a user sends a request, it saves a lot of processing time and power thereby improving performance. Moreover, by limiting the maximum number of idle connections, and setting suitable idle timeouts, enormous amounts of resources can be conserved. This not only improves performance but also makes the application more scalable. For instance, consider an e-commerce website during a huge sale where thousands of users are constantly connecting and disconnecting. By leveraging optimized connection pool settings, the application can process user requests more efficiently and faster, thus enhancing the site's overall backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Connection Pooling: Reducing Connection Overhead
|
||||
|
||||
Effective backend performance in web applications heavily relies on proficiently managing database connections, for which connection pooling plays a crucial role. When a web application needs to establish multiple connections or reconnect frequently, high overhead can become burdensome and slow down performance. Utilizing connection pools addresses this issue by enabling applications to reuse existing connections, rather than needing to establish a new one for each user or session that needs database access. For instance, in a high traffic eCommerce website, leveraging connection pooling can significantly reduce lag in loading product details or processing transactions, resulting in a smoother user experience and increased operational efficiency. By reducing connection overhead through connection pooling, backend performance is greatly enhanced, leading to an optimized and expedited data exchange process.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Managing Network Issues: Setting Appropriate Connection Timeouts and Implementing Efficient Retry Mechanisms
|
||||
|
||||
Efficient management of network issues directly contributes to enhanced backend performance in web applications. When an application attempts to establish a network connection, a reasonable connection timeout ensures the process doesn't hang indefinitely while waiting for a response. This allows for optimal system resource utilization, reducing unnecessary load on the server, thereby enhancing backend performance. For example, a server dealing with heavy traffic might cause delays. If the connection timeout is set too low, the application might terminate the process prematurely, reducing efficiency. Meanwhile, an effective retry mechanism is crucial to handle network failures. Without an efficient retry mechanism, network failures could trigger serious system errors or downtime. For example, if a network call fails due to temporary network issues, a well-implemented retry mechanism can attempt at re-establishing the connection, ensuring uninterrupted backend operations and enhanced application performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimizing Critical Paths and Frequently Accessed Endpoints
|
||||
|
||||
In web applications, maintaining the overall system health is crucial, and an important aspect of this is the optimization of critical paths and frequently accessed endpoints. These paths and endpoints act as the vital junctions where most user requests are processed, converted, and delivered as output. Proper identification and optimization of these routes ensure seamless user experience and high-speed data delivery. For instance, when a user logs on to an e-commerce website, the critical paths may include user authentication, product search, and payment gateway. Prioritizing the performance of these backend endpoints helps in reducing latency and enhances page load speed, preserving optimum overall system health.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Efficient Database Indexing
|
||||
|
||||
In web application development, efficient database indexing is instrumental in boosting backend performance. Indexes significantly cut short the time it takes for databases to retrieve and write data by providing swift navigational access to the rows in a table. For instance, a database without indexes may need to scan every row in a table to retrieve the required data, resulting in slow query response time. However, if the table is indexed, the same database can locate the data quickly and efficiently. It's akin to finding a book in a library - without a cataloguing system (index), you'd have to go through each book manually. With a cataloguing system (index), you can swiftly locate the exact book you need. Therefore, proper indexing strategy is key for high backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Denormalizing Database Schema for Read-Heavy Workloads and Reducing Join Operations
|
||||
|
||||
Web applications with high read demand benefit from a denormalized database schema, as it significantly improves backend performance. Primarily, denormalization reduces the need for costly join operations, making data retrieval quicker and more efficient. For example, an e-commerce application with millions of views per day would benefit from denormalized schema because each product page view might need to fetch data from multiple tables such as product, reviews, price, and vendor details. If these tables are denormalized into a single table, it eradicates the need for join operations, making the page load faster for end users. The subsequent boost in efficiency benefits the backend system by alleviating processing strain and enables it to deal with higher volume loads, thus enhancing overall backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Enabling Compression for Responses
|
||||
|
||||
Optimizing the backend performance of web applications often necessitates the enablement of compression for responses. Compression methods, such as Gzip or Brotli, reduce the size of the data transmitted between the server and the client. This result in faster data transfer, minimizing the load time of the web page and improving the user experience. For instance, if a web page has a size of 100 KB, applying compression can reduce it to 30 KB. This means less data to download, hence quicker loading times. Therefore, enabling compression for responses is critical in making web applications more efficient and responsive.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Efficient Pagination for Large Datasets
|
||||
|
||||
Handling large datasets effectively is key to improving backend performance in web applications. When a database query returns too much data, it can lead to slow load times and a poor user experience. Implementing efficient pagination significantly reduces the amount of data to be processed at once, thus reducing server load and latency times. For example, instead of loading thousands, or even millions, of records in one go, pagination allows it to load only a specific number of records per page, boosting speed and efficiency. It helps ensure seamless data retrieval, an impressive server response time, and ultimately better overall performance.
|
||||
@@ -1 +0,0 @@
|
||||
#
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimizing Join Operations and Avoiding Unnecessary Joins
|
||||
|
||||
In the realm of backend performance, the efficiency of join operations weighs heavily. Join operations combine rows from two or more tables, an action that can be processor-intensive and can drastically slow down system response times. As the size and complexity of databases increase, so does the time taken for these operations. Hence, optimizing join operations is paramount. This could involve appropriately indexing your tables or using specific types of joins such as INNER JOIN or LEFT JOIN depending on your needs. Similarly, unnecessary joins can clutter system processes and slow down performance. For example, if two tables have no real association but are joined, data retrieval can become sluggish and inefficient. Hence, preventing unnecessary joins enhances the overall backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utilization of HTTP Keep-Alive for Reducing Connection Overhead
|
||||
|
||||
Significant enhancement in backend performance for web applications can be achieved through the utilization of HTTP keep-alive. This protocol allows for multiple HTTP requests to be sent over the same TCP connection. Typically, each new request from a client to a server would require a new TCP connection, and this process can be resource-intensive and slow as it involves a three-way handshake. With HTTP keep-alive, these overheads are greatly reduced as one connection can be reused for multiple requests. For example, in a web application where users constantly interact and request data, using this method can greatly speed up the load time and response, creating a smoother user experience.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimizing Data Retrieval with Lazy Loading, Eager Loading, and Batch Processing
|
||||
|
||||
Optimizing data retrieval has a direct impact on backend performance in web applications. Specifically, features such as lazy loading, eager loading, and batch processing can greatly improve system responsiveness. Lazy loading, which entails loading data only when it's genuinely needed, can facilitate quicker initial page loading, thus improving user experience. On the contrary, eager loading minimizes the number of database queries by loading all necessary data upfront. While it may delay the initial loading process, it significantly speeds up subsequent data retrievals. In a similar vein, batch processing groups and executes similar tasks together, reducing the overhead associated with starting and ending tasks. These techniques are therefore crucial, as they help avoid performance bottlenecks and maintain efficient, seamless operation on the backend.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Load Balancing for Traffic Distribution
|
||||
|
||||
The performance of a backend system in web applications hugely relies on the way it handles incoming traffic. If a server is overwhelmed with too much traffic, it may slow down significantly or, in the worst-case scenario, crash completely. Opting to use load balancing mitigates these risks. Load balancing involves distributing network traffic across multiple servers, thereby ensuring none is overwhelmed. This undoubtedly optimizes backend performance, maintaining system stability, and increasing the capacity to handle more traffic. For instance, high traffic websites like Amazon and Facebook use load balancers to evenly distribute millions of requests per day among countless servers, ensuring smooth and efficient service delivery.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utilizing Message Brokers for Async Communication Between Services
|
||||
|
||||
Backend performance enhancement heavily relies on effective communication between diverse services. Message brokers, in this context, prove to be an essential instrument as they facilitate asynchronous communication, a method which boosts the system’s overall performance by allowing multiple operations to occur simultaneously. For instance, in a web application that processes online payments, a message broker can permit the receipt of payments (one service) to occur concurrently with updating the user’s payment history (another service). This prevents delays and halts, which means end users receive faster and smoother experiences. An improved backend performance, characterized by efficiency and time-effectiveness, makes this possible.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Comprehensive Monitoring and Logging
|
||||
|
||||
Backend performance can significantly affect the functionality and user experience of a web application. This necessitates the implementation of comprehensive monitoring and logging to track performance metrics and troubleshoot issues. These tactics give us eyes and ears within the performance of our application's infrastructure, helping identify potential bottlenecks or breakdowns. For example, monitoring could reveal that a particular database operation is taking longer than expected, which could be the cue to optimize the associated query. Similarly, logging will give us detailed records of application events, allowing us to trace and resolve any errors or issues captured in these logs. Unresolved issues can often slow down backend operations, or hamper their working altogether, hence impacting performance. Therefore, effective application of monitoring and validating logging data can enhance backend efficiency and bring valuable insights for further improvement.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Hosting Backend Close to Users to Minimize Network Latency
|
||||
|
||||
In web applications, reducing network latency can substantially enhance the backend performance. This means data has less distance to travel between users and servers, resulting in faster response times and smoother user experiences. For instance, if a company's primary user base resides in Asia but its server is in North America, the geographical gap can cause noticeable delays. However, by situating the backend near this Asia-based user base, data doesn't have to cross oceans and continents, making interactive web services more responsive and reliable. Hence, hosting the backend location close to the users is a crucial strategy in minimizing network latency.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Offloading Heavy Tasks to Background Jobs or Queues
|
||||
|
||||
In web applications, backend performance can be significantly optimized through the offloading of heavy tasks to background jobs or queues. If significant computational tasks or resource-intensive operations are processed in real-time, there can be a considerable slowdown in the system’s response time. This can lead to an undesirable user experience as requests take longer to process. In contrast, moving these heavy tasks to background processes allows for a more streamlined and efficient operation. For instance, creating a thumbnail for an uploaded image or sending a confirmation email could be moved to a background job, leaving the main thread free to handle user requests. This way, the user wouldn't have to wait unnecessarily and could continue navigating the website seamlessly, hence, improving overall system performance and responsiveness.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimization of Algorithms and Data Structures Used
|
||||
|
||||
Efficient use and optimization of algorithms and data structures significantly contribute to improving backend performance in web applications. For instance, a well-optimized sorting algorithm can enhance data processing speed while providing quick access to information. In contrast, an inefficient algorithm can increase server load leading to slowdowns and higher response times. Similarly, using appropriate data structures reduces memory usage and enhances data management. A classic example is using hash tables for efficient search operations instead of an array, reducing the time complexity from O(n) to O(1). Therefore, optimizing algorithms and data structures is essential for competent backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Fine-Tuning ORM Queries
|
||||
|
||||
For backend performance in web applications, one must diligently monitor and fine-tune Object-Relational Mapping (ORM) queries. Why? ORMs help to convert data between incompatible types, enabling database manipulations using an object-oriented paradigm. However, they can also generate heavy, inefficient SQL queries without proper management, creating lag in web performance. By keenly watching and fine-tuning these queries, you can ensure a smoother and faster data retrieval process, resulting in an overall boost to backend performance. For instance, ORM functions like eager loading and batch loading can be used to fetch related data in fewer queries, reducing load times and enhancing performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Efficient Pagination for Large Datasets
|
||||
|
||||
Backend performance in web applications can significantly be improved with efficient pagination for large datasets. When data-loaded in an application is quite extensive, executing simple queries without pagination can slow down response times, producing an adverse user experience. Through pagination, applications can deliver data in smaller, manageable chunks, reducing the amount of data transferred on each request and thereby increasing the speed and performance of the backend. For instance, instead of retrieving a million records at once, the application retrieves chunks of 50 or 100 at a time, dramatically enhancing the performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Regular Performance Testing and Benchmarking
|
||||
|
||||
Maintaining optimal backend performance in web applications involves consistent and regular performance testing and benchmarking. This practice helps in pinpointing any performance regressions which could otherwise significantly slow down the applications, leading to a subpar user experience. For example, if a new feature introduces memory leaks, regular testing can catch it before the feature is deployed. It also highlights improvements and illustrates the actual impact of optimization efforts over time. Through regular testing, ineffective optimizations can be scrapped before too many resources are invested into them, while beneficial strategies can be identified and further fine-tuned. Consequently, these actions contribute to a more efficient and productive application performance management strategy.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Optimising Backend Performance through Prefetching or Preloading Resources
|
||||
|
||||
The optimisation of backend performance in web applications involves proactively fetching or loading resources, data, or dependencies needed for future requests. By performing these operations in advance, costly delays (latency) are reduced significantly. This process ensures that resources are available as soon as they are required, resulting in a seamless and faster interaction for users. For instance, when a user opens a site, if images or other data that are likely to be used next are already preloaded, the user will not experience any delay as these elements load. As such, prefetching or preloading is critical to improve the overall speed of a web application, directly enhancing user experience.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Identifying Performance Bottlenecks through Code Profiling
|
||||
|
||||
The effective performance of a web application's backend heavily relies on the smooth operation of its code. Profiling is the process of monitoring the behaviour of your code, including the frequency and duration of function calls. This allows for the identification of performance bottlenecks—specific parts of the code that impede optimal performance. For example, a function that requires significant processing power and slows down the application can be revealed through code profiling. By identifying and resolving these bottlenecks, the backend performance can be dramatically improved, leading to faster response times and enhanced user experience.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Use of Profiling Tools in Database Management
|
||||
|
||||
The backend performance of web applications can greatly benefit from the utilization of profiling tools provided by the database. These tools allow for the identification and isolation of performance bottlenecks within an application. By identifying slow queries or areas of inefficient data retrieval, detection of these issues early-on can prevent the propagation of defects through the application, ultimately enhancing user experience. For instance, MySQL features a database profiling tool that can identify query performance through examination of query execution times. Profiling not only contributes to maintaining the speed and efficiency of a website, but also enables developers to optimize their code more effectively, saving valuable development time and resources.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Backend Monitoring with Prometheus, Grafana, ELK Stack
|
||||
|
||||
Efficiency and rate of performance are paramount for the backend processes in web applications. Utilizing performance monitoring tools such as Prometheus, Grafana, and the ELK Stack ensures that any issues impacting performance can be promptly identified and rectified. For example, Prometheus offers robust monitoring capabilities by collecting numeric time series data, presenting a detailed insight into the application's performance metrics. Grafana can visualize this data in an accessible, user-friendly way, helping developers to interpret complex statistics and notice trends or anomalies. Meanwhile, the ELK Stack (Elasticsearch, Logstash, Kibana) provides log management solutions, making it possible to search and analyze logs for indications of backend issues. By using these tools, developers can effectively keep backend performance at optimal levels, ensuring smoother user experiences.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Enforcing Reasonable Payload Size Limits
|
||||
|
||||
Backend performance in web applications largely depends on how quickly servers are able to process, store, and retrieve data. When large data payloads are transferred, it places a heavy strain on network resources and the server itself; potentially resulting in sluggish response times and poor application performance. Hence, enforcing reasonable payload size limits is vital to maintain optimum performance. For example, a web application dealing with large image files can implement limits to ensure that users don't upload images beyond a certain size. This not only helps to keep server and bandwidth costs manageable, but also ensures that the application runs smoothly for all users.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Regular Auditing and Updating Security Measures
|
||||
|
||||
Securing the backend of your web application is paramount to maintaining peak performance. If a system is compromised due to outdated security measures, hackers could leverage this access to disrupt the performance of the site. For instance, an attacker may deploy a DDoS attack, rendering the service slow or completely unavailable. By conducting regular audits and updates of security measures, possible vulnerabilities can be identified and solved before they turn into larger performance affecting issues. This proactive approach supports stable operation, ensures smooth access for users, and promotes overall backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Database Replication for Redundancy and Enhanced Read Performance
|
||||
|
||||
Safeguarding backend performance necessitates database replication, as it increases redundancy thus enhancing data consistency across different systems. It facilitates simultaneous access to the same data from various servers, which significantly optimizes read performance. This is particularly beneficial for web applications that experience substantial read loads. For example, consider a busy e-commerce site during a sales event. If all read and write operations occur on the same database, it could lead to performance lags. However, with database replication, such high-volume read operations can be redirected to replicated servers, assuring smooth and efficient customer experiences.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Implementing Request Throttling and Rate Limiting
|
||||
|
||||
In the context of backend performance, implementing request throttling and rate limiting acts as a defensive mechanism against system overload. These practices help in managing the flow of incoming requests to a level that the system can handle comfortably, thereby improving responsiveness and reliability. For instance, during a high traffic spike, uncontrolled, simultaneous requests might exhaust system resources leading to service disruption. However, with request throttling and rate limiting, you can control this traffic ensuring a steady performance. Furthermore, it also provides a layer of security by thwarting potential DDoS attacks which aim to flood the system with requests leading to a system crash.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Proper Implementation of Horizontal or Vertical Scaling
|
||||
|
||||
An optimal backend performance in web applications relies heavily on implementing the right type of scaling, whether horizontal or vertical. In vertical scaling, additional resources are added to increase the capacity of an existing machine. It helps in the short run by quickly accommodating an increased load, but may be limited by the maximum capacity of individual servers. In contrast, horizontal scaling provides longer-term scalability by adding more machines to the existing pool. This improves the redundancy and reliability of the application and can handle significantly larger loads without relying on high-spec servers. A careful balance or judicious use of both can drastically improve backend performance. For example, a sudden surge in website traffic can be swiftly managed with vertical scaling while consistent long-term growth can be accommodated with horizontal scaling. Therefore, the decision of using horizontal or vertical scaling is pivotal in determining backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Data Optimization: Avoid Select * Queries and Fetch Only Required Columns
|
||||
|
||||
Efficiency in the backend of web applications can be significantly improved by careful data queries. By avoiding the use of "Select *" queries, and instead only fetching the necessary columns, you reduce the load and strain on the database. This can not only accelerate the response time, but also reduces the storage usage, thereby improving the overall performance. To illustrate, consider a large database with hundreds of columns; using "Select *" would fetch all that data unnecessarily when you might only need data from three or four columns. This smart selection contributes immensely to a more optimal backend performance.
|
||||