Compare commits
8 Commits
feat/check
...
feat/open-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7238171fa | ||
|
|
b5b34504c6 | ||
|
|
1e044405a4 | ||
|
|
72d697f6d7 | ||
|
|
57d86542e0 | ||
|
|
36e8e6051b | ||
|
|
4f9917bc5c | ||
|
|
d1b27854ea |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": 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: 314 KiB After Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 881 KiB |
@@ -74,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
|
||||
|
||||
@@ -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,7 +48,7 @@ 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'>@kamrify</span>
|
||||
|
||||
@@ -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-center rounded p-2 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 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,7 +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();
|
||||
|
||||
@@ -129,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);
|
||||
|
||||
@@ -174,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,
|
||||
});
|
||||
}
|
||||
@@ -199,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;
|
||||
}
|
||||
|
||||
@@ -299,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}`);
|
||||
|
||||
@@ -320,7 +313,7 @@ export function GenerateRoadmap() {
|
||||
data,
|
||||
});
|
||||
|
||||
setRoadmapTerm(term);
|
||||
setRoadmapTerm(title);
|
||||
setGeneratedRoadmapContent(data);
|
||||
visitAIRoadmap(roadmapId);
|
||||
};
|
||||
@@ -367,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;
|
||||
@@ -409,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);
|
||||
}}
|
||||
@@ -497,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>
|
||||
)}
|
||||
|
||||
@@ -532,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'}
|
||||
@@ -554,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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -657,48 +632,11 @@ export function GenerateRoadmap() {
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn({
|
||||
'relative mb-20 max-h-[800px] min-h-[800px] sm:max-h-[1000px] md:min-h-[1000px] lg:max-h-[1200px] lg:min-h-[1200px] overflow-hidden':
|
||||
!isAuthenticatedUser,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
ref={roadmapContainerRef}
|
||||
id="roadmap-container"
|
||||
onClick={handleNodeClick}
|
||||
className="relative 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 px-5 max-w-[600px] flex-col items-center justify-center bg-gray-100 pt-px">
|
||||
<div className="mt-8 text-center">
|
||||
<h2 className="mb-0.5 sm:mb-3 text-xl sm:text-2xl font-medium">
|
||||
Sign up to View the full roadmap
|
||||
</h2>
|
||||
<p className="mb-6 text-sm sm:text-base text-gray-600 text-balance">
|
||||
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,23 +38,16 @@ 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">
|
||||
{isConfiguring && (
|
||||
<IncreaseRoadmapLimit
|
||||
<OpenAISettings
|
||||
onClose={() => {
|
||||
setOpenAPIKey(getOpenAIKey()!);
|
||||
setIsConfiguring(false);
|
||||
loadAIRoadmapLimit();
|
||||
}}
|
||||
@@ -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">
|
||||
|
||||
@@ -40,7 +40,7 @@ const { frontmatter, author } = guide;
|
||||
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'>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Minimizing Overhead Through Batch Processing
|
||||
|
||||
The capacity of a web application's backend to process large volumes of data promptly and efficiently plays a crucial role in its performance. Grouping similar requests together in a batch, rather than processing them individually, considerably reduces data transfer overhead. This is because it minimizes the number of round trips, or interactions between the client and server to obtain a response. For instance, in an e-commerce application, instead of retrieving each product detail individually, batching gathers all product details in a single request, which enhances response times and overall performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Slow-Query Logging and Regular Monitoring
|
||||
|
||||
Keeping tabs on slow-query logging is vital for backend performance since it can help in identifying inefficient queries that may drag down the server's productivity. For instance, a slow query might be taking an exceptionally long time to navigate through a large database, causing delays in information retrieval. By enabling slow-query logging, such ineffective queries can be spotted and optimized or reworked to minimize their run-time. Thus, it aids in maintaining smooth and efficient server operation while enhancing the application's overall performance. Continuous monitoring of these logs can also point out recurring issues that need addressing, making it an indispensable tool for optimizing backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Streaming of Large Requests/Responses
|
||||
|
||||
In web application backend performance, the implementation of streaming large requests and responses is essential to maximize efficiency and speed. This is because streaming, unlike traditional methods, doesn't require the entire file to load before it can be accessed. This means that large data pieces are broken down into more manageable, smaller chunks which are then processed separately. Streaming minimizes memory usage, prevents potential timeouts, and reduces the latency between the client and server. For instance, when streaming a video, the user doesn't have to wait for the full video to buffer, hence enhancing user experience by delivering content faster and more seamlessly.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Minimising Unnecessary Processing or Expensive Computation on the Server
|
||||
|
||||
Efficient backend performance in web applications is often determined by how well unnecessary processing or expensive computations are minimised on the server. When an application is free of excess processing or complex computations, it expends less energy, executes tasks swiftly, and reduces any potential downtime. This remarkably improves the application's response time to user requests. For example, instead of calculating the same data repeatedly for different users, the application can calculate once, store the result, and then provide this stored result upon user request. This essentially minimises unnecessary processes, thereby enhancing the web application's backend performance.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Maintaining Updated Dependencies
|
||||
|
||||
Keeping your dependencies up to date is crucial for optimizing backend performance in web applications. Regular updates bring new features, improvements, and important patches for security vulnerabilities that could harm the performance and security of your application. An outdated package, for example, may run inefficiently or even prevent other components from functioning at peak performance. This creates a ripple effect that could slow down or disrupt entire processes. Therefore, staying current with all updates enhances the robustness and operational efficiency, contributing to faster load times, better stability, and ultimately, an improved user experience.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Database Sharding for Data Distribution
|
||||
|
||||
When it comes to backend performance, the effectiveness of data management is critical. Here lies the value of database sharding, a type of database partitioning that separates very large databases into smaller, faster, more easily managed parts called data shards. Sharding can enhance the speed of data retrieval by spreading the load across multiple servers, thereby reducing bottlenecks and improving overall application responsiveness. For instance, in an e-commerce application with a worldwide customer base, data can be sharded on a geographical basis to ensure faster loading times for consumers, no matter where they are located. This improves user experience and ensures smooth operation on the backend side.
|
||||
@@ -1,3 +0,0 @@
|
||||
# Utilizing Caching Mechanisms
|
||||
|
||||
Backend performance of web applications can see substantial improvements when effective caching mechanisms, like HTTP, server/client, and CDN are properly implemented. Caching can significantly decrease the load on the server, minimising the effort necessary to generate a response to a user's request. For example, when a user revisits a previously accessed web page, caching mechanisms retrieve stored information more swiftly than the server could generate it. This process effectively cuts down on latency, bandwidth usage and processing power, speeding up the loading times and contributing to smoother user experience. CDN-based caches also help in serving static resources to users from the nearest possible location, reducing network latency.
|
||||
@@ -6,7 +6,7 @@ excludedBySlug: '/backend/developer-skills'
|
||||
seo:
|
||||
title: '8 In-Demand Backend Developer Skills to Master'
|
||||
description: 'Learn what the essential backend developer skills are that you should learn and master to advance in your career.'
|
||||
isNew: false
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2024-02-27
|
||||
sitemap:
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
---
|
||||
title: '25 Essential Backend Development Tools for 2024'
|
||||
description: 'Elevate your development process with these 25 essential backend developer tools.'
|
||||
authorId: fernando
|
||||
excludedBySlug: '/backend/developer-tools'
|
||||
seo:
|
||||
title: '25 Essential Backend Development Tools for 2024'
|
||||
description: 'Elevate your coding with backend developer tools that bring efficiency, scalability, and innovation to your projects. Improve your development process today!'
|
||||
isNew: true
|
||||
type: 'textual'
|
||||
date: 2024-03-19
|
||||
sitemap:
|
||||
priority: 0.7
|
||||
changefreq: 'weekly'
|
||||
tags:
|
||||
- 'guide'
|
||||
- 'textual-guide'
|
||||
- 'guide-sitemap'
|
||||
---
|
||||
|
||||
|
||||
As developers, we’re not just writing code on a text editor without any other external help. Whether we realize it or not, we’re constantly using different development tools to improve the way we work and the speed at which we can deliver our code.
|
||||
|
||||
In this article, we’ll cover 25 backend development tools that are crucial in the web development industry, and as a [backend developer](/backend), you should be aware of them.
|
||||
|
||||
The categories we’ll tackle are:
|
||||
|
||||
- IDEs and Editors
|
||||
- Database Tools
|
||||
- Collaboration
|
||||
- Hosting Services
|
||||
- API-Related Tools
|
||||
- Productivity
|
||||
|
||||
So let’s get started!
|
||||
|
||||
## Beyond programming languages: IDEs and Editors
|
||||
|
||||
Other than the actual programming languages, the Integrated Development Environment (A.K.A your IDE) is the single most important tool you’ll have to pick and use throughout your career in software development.
|
||||
|
||||
Some of them are generic (as in, they work for all types of development), and others will have specific backend development tools (which is what we’re focusing on right now).
|
||||
|
||||
Let’s see some examples that are great for a web development project.
|
||||
|
||||
### 1. Jetbrains Products
|
||||
|
||||
The [Jetbrains family](https://www.jetbrains.com/) of IDEs targets multiple programming languages, including JavaScript, .NET, JAVA (and the Java Virtual Machine), Python, PHP, and more (mostly great options for web development).
|
||||
|
||||

|
||||
|
||||
The benefit of using these IDEs, in comparison with others, is that given how they’re language/technology specific, they have tools designed for those programming languages and specifically for this list to help in your backend development tasks, such as:
|
||||
|
||||
- Debuggers.
|
||||
- Improved IntelliSense.
|
||||
- Improved development environment.
|
||||
|
||||
The only minor issue with these IDEs, especially when compared to the rest of the options listed here, is that they’re not all free. While not all of them are priced the same, I recommend you check out your IDE’s pricing page to understand what options you have (there are free plans if you qualify for them).
|
||||
|
||||
### 2. Visual Studio Code
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) is definitely one of the most popular alternatives these days for all types of web developers, but definitely for backend developers. This IDE’s strongest selling point is that it’s incredibly extensible through plugins. And the community using it is so big and varied that there are plugins for literally anything you need.
|
||||
|
||||

|
||||
|
||||
The other major benefit of VSCode over JetBrains products is that it gives developers a fully working IDE for FREE. While some of the extensions don’t provide exactly the same developer experience as a JetBrains IDE, the proper combination of extensions can provide a very close alternative through VSCode.
|
||||
|
||||
### 3. Zed
|
||||
|
||||
[Zed](https://zed.dev/) is a different type of code editor, and because of that, it might just be the right one for you.
|
||||
|
||||

|
||||
|
||||
Zed, like VSCode, is an all-purpose code editor, letting you code in whatever language you want (whether you’re doing web development or not). The main reasons why you’d pick Zed over others are:
|
||||
|
||||
- **Improved performance.** Zed takes advantage of your CPU AND GPU to improve the speed at which the IDE responds to your commands.
|
||||
- **Multi-user support.**
|
||||
- **Team features.** Zed lets you build software while working with others by sharing notes and letting you interact with teammates through the IDE.
|
||||
|
||||
Zed is an [open-source project](https://github.com/zed-industries/zed), but at the time of writing this article, it’s only available for macOS, so Linux and Windows users are still unable to try this excellent option.
|
||||
|
||||
### 4. Sublime Text
|
||||
|
||||
Before VSCode, [Sublime Text](https://www.sublimetext.com/) was probably one of the most popular code editors for web developers who wanted something powerful for free.
|
||||
|
||||
Just like VSCode, Sublime supports extensibility through plugins, and the rich ecosystem of plugins makes it quite a versatile editor. As a note, this code editor also supports GPU rendering of the UI, like Zed does, so if performance is important to you, then you’ll want to keep reading.
|
||||
|
||||

|
||||
|
||||
With a refreshed UI (if you’ve used Sublime Text in the past, you’ll be happily surprised!) and better internal tooling, the latest version of this editor (version 4) is trying to regain the portion of the market that VSCode took from it at the time.
|
||||
|
||||
### 5. VIM - a developer-focused editor
|
||||
|
||||
[VIM](https://www.vim.org/) is a tool that you either hate or love as a developer, but there is no middle ground.
|
||||
|
||||
This is such an iconic text editor that all the previously mentioned IDE have what is called a “vim mode,” which allows you to use them as if you were using VIM (with the visual and input modes).
|
||||
|
||||

|
||||
|
||||
Vim lets you write code without having to move your fingers away from the home row (the row where you “rest” your fingers, the one with the F and G keys). That means you can navigate documents, write code, move through projects, and more, all with minimum hand movement.
|
||||
|
||||
This is the key philosophy behind Vim’s design, and if you embrace it, it should help to make you a very proficient developer. Of course, adapting to this way of working is not trivial, and there is a lot of muscle memory that has to be re-trained. But once you do it, it’s really hard to go back.
|
||||
|
||||
Just like with all the generic IDEs here, you’ll have to [customize it through “scripts”](https://www.vim.org/scripts/script_search_results.php?order_by=creation_date&direction=descending) to make it work exactly as you want for your environment.
|
||||
|
||||
## Database Tools
|
||||
|
||||
While doing backend development, you will definitely be interacting with databases. They’re a ubiquitous backend tool in the realm of web development.
|
||||
|
||||
Let’s take a look at some great database tools you can use as a backend developer to interact with your favorite database management systems (DBMS).
|
||||
|
||||
### 6. DataGrip
|
||||
|
||||
[Datagrip](https://www.jetbrains.com/datagrip/) is a JetBrains product, which makes it a great option if you’re also going with a JetBrains IDE.
|
||||
|
||||
This tool lets you access all SQL databases from within the same user interface, it provides great help while browsing the data stored in the database, and it also has features that help you write better SQL queries.
|
||||
|
||||

|
||||
|
||||
While the pricing of these tools might be a bit steep (especially if you go with the IDE as well), it’s definitely a solid option if you’re looking for feature-rich and stable software development tools.
|
||||
|
||||
### 7. Navicat
|
||||
|
||||
[Navicat](https://navicat.com/en/products) actually has a family of alternatives based on what you need, from the standard set of SQL databases (such as MySQL, Oracle, Postgre, and so on) up to other NoSQL databases such as MongoDB and Redis.
|
||||
|
||||
In general, the Navicat alternatives are quite lightweight and powerful to use. They might not be as feature-rich as Datagrip, but they let you easily browse and query the data you need.
|
||||
|
||||

|
||||
|
||||
The free trial for Navicat only lasts 14 days, and then you’ll have to pay a monthly fee. That said, for non-commercial use, the license is quite low, which makes it accessible to almost all developers.
|
||||
|
||||
As for features, it has all the ones you’d expect from a tool like this:
|
||||
|
||||
- Ability to connect to multiple databases and visually browse their content.
|
||||
- Query editor with some IntelliSense built-in.
|
||||
- Model representation (automatically converts a database into ER Diagrams).
|
||||
- Simple object designer to create entities through a visual IDE.
|
||||
|
||||
The Navicat series of apps are great backend tools to have at your disposal, if you don’t mind their price, that is.
|
||||
|
||||
### 8. TablePlus
|
||||
|
||||
[Tableplus](https://tableplus.com/) is very similar to Navicat in the sense that it’s another lightweight database manager. The main differences are:
|
||||
|
||||
- Tableplus only supports SQL-based databases.
|
||||
- The pricing model is simpler, by only charging a one-time license without you having to commit to a monthly subscription.
|
||||
|
||||

|
||||
|
||||
Some of the most relevant features of Tableplus are:
|
||||
|
||||
- Inline data editing.
|
||||
- Advanced filtering lets you combine different filtering conditions when searching.
|
||||
- Code auto-complete, which comes in very handy when writing SQL queries.
|
||||
- Extensible through JavaScript plugins (currently in Beta).
|
||||
|
||||
This is a solid backend tool alternative to any of the previous options and with a simpler price tag.
|
||||
|
||||
### 9. DBeaver
|
||||
|
||||
[DBeaver](https://dbeaver.io/) is a free, cross-platform tool that lets you connect and interact with multiple databases. While there is a PRO version with extra features, the free version is more than powerful enough to get you started with almost any database you can think of, both SQL and NoSQL alike.
|
||||
|
||||
For a full list of supported databases on the free version, check out their [about page](https://dbeaver.io/about/).
|
||||
|
||||

|
||||
|
||||
Some of the major features of DBeaver are:
|
||||
|
||||
- SQL editor with completion.
|
||||
- ER-diagram creation from a table definition.
|
||||
- In-line data editing.
|
||||
|
||||
Task management to kill any long-lasting queries that block your database.
|
||||
|
||||
## Collaboration tools for web development
|
||||
|
||||
Unless you’re working as a solo-dev (and even then!), collaboration tools allow you to coordinate your work and understand who’s working on what and what you should be working on next.
|
||||
|
||||
While these might not be considered “backend tools” per se, they definitely help improve your performance and organization, so we can still call them “development tools” as a broader term.
|
||||
|
||||
### 10. Trello
|
||||
|
||||
[Trello](https://trello.com/) is a very simple yet powerful organizational tool that lets teams build a Kanban-like board with clear states and simple UX (drag&drop is king in Trello).
|
||||
|
||||
Setting up a new project and a team takes minutes in Trello, and through the plugin system, you can get extra features such as date reminders, calendar integrations, and more.
|
||||
|
||||

|
||||
|
||||
The simple UI and intuitive UX make Trello one of the best options out there for collaborative tools as long as the needs of the team are met with simple column-based layouts and minimal information.
|
||||
|
||||
### 11. Monday
|
||||
|
||||
[Monday](https://monday.com/) is a relatively new online platform for project management and collaboration. I say “new” because some of the other alternatives here have been around for over 5+ years.
|
||||
|
||||
Their limited free plan lasts forever, so if you have a small team and limited requirements, this might just be the perfect tool for you. Also, if you actually need to pay, Monday’s plans are accessible, especially when compared to other alternatives.
|
||||
|
||||

|
||||
|
||||
Monday’s fully customizable UI lets you build the collaborative environment you need. This is a huge advantage over others who've been around for longer and have a fixed UI that you have to adapt to.
|
||||
|
||||
### 12. Basecamp
|
||||
|
||||
[Basecamp](https://basecamp.com/) is a mix between Trello, Monday, and Notion in the sense that it tries to provide developers with the best and most relevant tools from those worlds, leaving out the ones that just create “noise.”
|
||||
|
||||

|
||||
|
||||
Basecamp’s philosophy is to keep things simple and only focus on the features that truly add to collaboration:
|
||||
|
||||
- Simple card tables like Trello.
|
||||
- Ability to upload and manage documents and files with your team.
|
||||
- Integrated chat.
|
||||
- Message boards to send notifications to everyone.
|
||||
|
||||
The only “downside” to basecamp, if you will, is that there is no “forever free” plan. Both their plans are paid and have a 30-day free trial, so you can definitely give it a shot and figure out if what they offer is enough for your needs.
|
||||
|
||||
### 13. Wrike
|
||||
|
||||
[Wrike](https://www.wrike.com/) is yet another attempt at making project management and collaboration feel organic and seamless. They have a minimalistic UI and provide you with over 400 integrations to create your own workflows based on your needs and current ecosystem.
|
||||
|
||||
They have a free plan that, while feature-limited, it’s perfect for understanding the basic way of using Wrike and how useful it can be to you in your current project.
|
||||
|
||||

|
||||
|
||||
Their innovative use of AI allows you to create content faster, analyze project and task descriptions, and create subtasks based on it.
|
||||
|
||||
Wrike feels efficient and powerful, even for its free plan. Give it a chance if you’re a freelancer or if you have a small team looking for something new and powerful.
|
||||
|
||||
## Hosting Services
|
||||
|
||||
When it comes to backend development, deploying your code and running it on the cloud will be a common thing; this is a practice known as continuous integration and continuous deployment (CI/CD). While in some situations, you’ll be dealing with a custom, in-house infrastructure, there are platforms that will make the entire process very lightweight (as in deploying with a couple of clicks).
|
||||
|
||||
Let’s take a look at some of the most common alternatives!
|
||||
|
||||
### 14. Railway
|
||||
|
||||
[Railway.app](https://railway.app/) aims at giving developers all the tools they need at a click’s distance. We’re talking about:
|
||||
|
||||
- PR-triggered deployments.
|
||||
- Support for all popular programming languages.
|
||||
- Autoscaling.
|
||||
- Load balancing.
|
||||
- Monitoring.
|
||||
- A great uptime (99.95%)
|
||||
- With more than 200 ready-made templates for you to get going.
|
||||
|
||||

|
||||
|
||||
Railway has no free plan, but their basic one is very accessible. Careful though, they also charge per resource utilization. Lucky for you, they have a [very handy consumption calculator](https://railway.app/pricing) to avoid surprises at the end of the month!
|
||||
|
||||
### 15. Heroku
|
||||
|
||||
[Heroku](https://www.heroku.com/) is another Platform as a Service provider. This one provides the basic services most of them do, such as autoscaling, monitoring, GitHub integration, and more.
|
||||
|
||||
The list of supported programming languages is not huge, but the most common ones are definitely covered: Node.js, Ruby, JAVA, PHP, Python, Go, Scala, and even Clojure.
|
||||
|
||||

|
||||
|
||||
Another great selling point for Heroku is that on top of their infrastructure, they also offer a managed Postgre database as a service and a Redis one. In both situations, you’ll have to pay for the services as you use them, so keep that in mind.
|
||||
|
||||
### 16. Digital Ocean
|
||||
|
||||
As opposed to platforms such as Heroku, [Digital Ocean](https://www.digitalocean.com/) is known as an Infrastructure as a Service provider (IaaS). They give you all the servers you need and all the resources (memory, CPU, etc) you want to pay for. However, setting up your deployment process, automating your integration tests, or even having all the required libraries to run your code is up to you.
|
||||
|
||||
This is by no means something bad, some teams do prefer to have that freedom over other platforms like Railway and Heroku, where everything’s already managed.
|
||||
|
||||

|
||||
|
||||
Large-scale applications will usually require to have custom infrastructure that managed services can hardly provide. This is where IaaS providers come in.
|
||||
|
||||
On top of their basic offering, they do offer managed databases such as MongoDB, MySQL, Redis, and others.
|
||||
|
||||
### 17. Hetzner
|
||||
|
||||
[Hetzner](https://www.hetzner.com/) is yet another IaaS that offers everything you need to get going if you know what to do with it. In other words, they offer all the hardware you might dream of, even in the cloud, but you have to configure it and maintain it.
|
||||
|
||||
Their only “managed” offer is for web hosting though, so if you’re looking to host your website or app and you don’t want to have to deal with server maintenance and configuration, then this is a good option for you.
|
||||
|
||||

|
||||
|
||||
Other than that, their offering is quite standard, although their pricing model might not be. While they do have the standard pricing tiers like the rest of them, they also give you the option to “bid” for used hardware that is no longer needed.
|
||||
|
||||
### 18. Vercel
|
||||
|
||||
If you’re building a NextJS application and you’re looking for a quick way to deploy it, then there is probably no better place than [Vercel](https://vercel.com/) (the owner of NextJS).
|
||||
|
||||
Their platform allows you to link your GitHub account to their systems and deploy your entire application with a single push to the repo.
|
||||
|
||||

|
||||
|
||||
And since they’re experts on NextJS, your app will “just work.”
|
||||
|
||||
Even their free plan is perfect for quick SaaS prototypes and small applications. On top of this, they offer monitoring, auto-scaling, load balancing, and everything you’d expect from a PaaS provider.
|
||||
|
||||
While it’s true they don’t support other technologies or even offer other related services, such as managed databases, there is hardly anyone who can provide a better developer experience when it comes to deploying a NextJS application.
|
||||
|
||||
### 19. Render
|
||||
|
||||
You can think of [Render](https://render.com/) as if Vercel and Heroku had a love child. Render gives you the amazing developer experience provided by Vercel but the flexibility (or more) from Heroku.
|
||||
|
||||
You’re not tied to a single technology; instead, you have all the major runtimes available out of the box. Much higher HTTP timeouts (up to 100 minutes, which is incredible compared to the standard 10 or 30 seconds most providers give you) and tons of other security and quality-of-life improvements.
|
||||
|
||||

|
||||
|
||||
Render also offers managed MySQL and managed Redis instances for you to use, even in their free tier. In the end, unless you’ve been using Heroku for a while and you’re happy with their DX, it might be a good idea to check out Render instead.
|
||||
|
||||
### 20. OVHCloud
|
||||
|
||||
[OVHCloud](https://www.ovhcloud.com/) is an all-in-one solution that seems to provide you with everything you need, from “bare metal” (as in infrastructure) to managed hosting for web applications, managed databases (they have many to choose from), and many other services.
|
||||
|
||||
However, they do not seem to offer quality-of-life integrations to make your deployment workflow simple and intuitive.
|
||||
|
||||

|
||||
|
||||
Now, given how they centralize all related services from domain name registration all the way up to analytics, identity management, file storage (CDN), and even one-click install CMS (content management systems, such as WordPress), etc, it might just be a good option for you. That is if you have the expertise in your team to deal with all these options.
|
||||
|
||||
## API-Related Tools
|
||||
|
||||
As backend developers, we’ll always be dealing with APIs (Application Programming Interface), either through using the ones created by others or writing our own.
|
||||
|
||||
Whatever you’re doing, it’s always good to have some backend tools to help you build and test them faster, so let’s take a look at a few options.
|
||||
|
||||
### 21. Swagger
|
||||
|
||||
Some developers would argue that one of the hardest parts of creating an API is documenting it. Not only because it might sound like a boring task, but explaining what the API endpoint is doing well enough is not trivial.
|
||||
|
||||
That’s where [Swagger](https://swagger.io/) comes into play.
|
||||
|
||||

|
||||
|
||||
This tool allows you to create interactive documentation that provides developers with all they need to understand how to use your endpoints, and at the same time, it also gives them the option to test them directly from the generated UI.
|
||||
|
||||
### 22. Postman
|
||||
|
||||
[Postman](https://www.postman.com/) is less of a documentation-only app and has grown over the years to become a testing API tool that every developer and development team should know about. Backend developers are especially benefited from using Postman because of how well it helps organize and manage APIs.
|
||||
|
||||
With Postman, you can organize all your company’s APIs, share them with the associated dev teams, and let them use and interact with them without having to write a single line of code.
|
||||
|
||||

|
||||
|
||||
While Swagger is more of a development tool that every backend developer should know about, Postman is the tool that every development team should use to share & control internal API access and documentation.
|
||||
|
||||
## Productivity
|
||||
|
||||
Finally, the last category is about productivity. While some of the products and services mentioned already do provide productivity enhancements, they weren’t there for that. The following list of backend tools is created thinking only about the benefits they can bring to your productivity while working as a backend developer.
|
||||
|
||||
### 23. iTerm
|
||||
|
||||
If you’re a macOS user, then [iTerm](https://iterm2.com/) is definitely one of the “must haves” you need to look into. As a backend developer, you’ll spend a lot of your day in the terminal.
|
||||
|
||||

|
||||
|
||||
iTerm will take that experience to the next level by bringing in features such as:
|
||||
|
||||
- Parallel panes inside the same window making it easy to multi-task.
|
||||
- Improved auto-complete
|
||||
- In-window search outside of the current program you’re using.
|
||||
- Instant replay lets you review the latest content on the screen before cleaning it.
|
||||
- Paste history, letting you move through the latest pasted content into the terminal.
|
||||
|
||||
Mind you, none of these features are mandatory; you can easily work without them, but they do improve your quality of life as a developer. Hence the reason why iTerm leads this list.
|
||||
|
||||
### 24. Zsh/OhMyZsh
|
||||
|
||||
The combination of these two gives your terminal superpowers. [Zsh](https://zsh.sourceforge.io/) is an improved shell that lets you work much faster and more efficiently if you’re spending several hours typing commands in your terminal. For example, you get features such as:
|
||||
|
||||
- Advanced tab auto-complete
|
||||
- Extensibility
|
||||
- Spelling corrections
|
||||
- And more.
|
||||
|
||||

|
||||
|
||||
As mentioned above, after you have your ZSH installed and set up, you should look into installing oh-my-zsh, which helps with configuring all the customization options you have on this shell:
|
||||
|
||||
- It comes bundled with [over 300 plugins](https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins), ranging from 1password integrations to the “jump” plugin, which lets you move around the filesystem by moving from mark to mark (you can assign marks to folders).
|
||||
- [Plenty of themes](https://github.com/ohmyzsh/ohmyzsh/wiki/Themes) out of the box.
|
||||
|
||||
If you find yourself spending hours on the terminal, consider installing this combo.
|
||||
|
||||
### 25. Raycast
|
||||
|
||||
[Raycast](https://www.raycast.com/) allows you to improve your productivity by giving you a better application launcher. Instead of using the default launcher, you can replace it with Raycast and gain superpowers.
|
||||
|
||||
Now, you suddenly have access to hundreds of [community-created extensions](https://www.raycast.com/store) that allow you to directly interact with chatGPT from the app launcher, use GitHub, interact with VSCode directly, and more.
|
||||
|
||||

|
||||
|
||||
While it is only available for macOS users, Raycast has become a must-have application for backend developers on this platform. In the end, the faster you can reach for your tools, the more productive you become. And a properly configured Raycast can make your web development process feel like a breeze.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -32,12 +32,5 @@ export const faqs: FAQType[] = [
|
||||
'If you are a beginner who is just getting started, don\'t feel overwhelmed by looking at this roadmap. Look at the answer to the FAQ "How to become a Backend Developer?"',
|
||||
],
|
||||
},
|
||||
{
|
||||
question: 'What tools does a backend developer use?',
|
||||
answer: [
|
||||
'Other than the language itself, some common [backend developer tools](https://roadmap.sh/backend/developer-tools) that you’ll use as a backend dev, are going to be: The IDE/Text editor; here you have tons of options from VSCode, to Zed or Sublime Text. Some database tools, such as DataGrid or Navicat interact with your databases from outside your code. API-related tools like Swagger or Postman will help you document their behavior and share the endpoints with your team. A hosting service for your code. And finally, some collaborative and productivity tools such as Jira, Wrike, Trello or Monday to help you organize your work with your colleagues. And others like Raycast or iTerm to boost your productivity. There are many alternatives in each category, it’s up to you to try them and pick the ones that work best for you.',
|
||||
|
||||
]
|
||||
}
|
||||
];
|
||||
---
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# How to Calculate Complexity?
|
||||
|
||||
The process of calculating algorithmic complexity, often referred to as Big O notation, involves counting the operations or steps an algorithm takes in function of the size of its input. The aim is to identify the worst-case, average-case, and best-case complexity. Generally, the main focus is on the worst-case scenario which represents the maximum number of steps taken by an algorithm. To calculate it, you consider the highest order of size (n) in your algorithm's steps. For instance, if an algorithm performs a loop 5 times for 'n' items, and then does 3 unrelated steps, it has a complexity of O(n), because the linear steps grow faster than constant ones as n increases. Other complexities include O(1) for constant complexity, O(n) for linear complexity, O(n^2) for quadratic complexity, and so on, based on how the steps increase with size.
|
||||
|
||||
- [Time & Space Complexity](https://www.youtube.com/watch?v=Z0bH0cMY0E8)
|
||||
The process of calculating algorithmic complexity, often referred to as Big O notation, involves counting the operations or steps an algorithm takes in function of the size of its input. The aim is to identify the worst-case, average-case, and best-case complexity. Generally, the main focus is on the worst-case scenario which represents the maximum number of steps taken by an algorithm. To calculate it, you consider the highest order of size (n) in your algorithm's steps. For instance, if an algorithm performs a loop 5 times for 'n' items, and then does 3 unrelated steps, it has a complexity of O(n), because the linear steps grow faster than constant ones as n increases. Other complexities include O(1) for constant complexity, O(n) for linear complexity, O(n^2) for quadratic complexity, and so on, based on how the steps increase with size.
|
||||
@@ -7,7 +7,7 @@ briefDescription: 'Step by step guide to learn Data Structures and Algorithms in
|
||||
title: 'Data Structures & Algorithms Roadmap'
|
||||
description: 'Step by step guide to learn Data Structures and Algorithms in 2024'
|
||||
hasTopics: true
|
||||
isNew: false
|
||||
isNew: true
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 1814.72
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Java Fundamentals
|
||||
|
||||
Java is a programming language and computing platform first released by Sun Microsystems in 1995. Java is a general-purpose, class-based, object-oriented programming language designed for having lesser implementation dependencies. It is a computing platform for application development. Java is fast, secure, and reliable. Therefore, it is widely used for developing Java applications in laptops, data centers, game consoles, scientific supercomputers, cell phones, etc.
|
||||
Java is a programming language and computing platform first released by Sun Microsystems in 1995. Java is a general-purpose, class-based, object-oriented programming language designed for having lesser implementation dependencies. It is a computing platform for application development. Java is fast, secure, and reliable, therefore. It is widely used for developing Java applications in laptops, data centers, game consoles, scientific supercomputers, cell phones, etc.
|
||||
|
||||
Learn about the fundamentals of Java such as basic syntax, data types, variables, conditionals, functions, data structures, packages, etc.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CDN Caching
|
||||
|
||||
A Content Delivery Network (CDN) is a distributed network of servers that are strategically placed in various locations around the world. The main purpose of a CDN is to serve content to end-users with high availability and high performance by caching frequently accessed content on servers that are closer to the end-users.
|
||||
A Content Delivery Network (CDN) is a distributed network of servers that are strategically placed in various locations around the world. The main purpose of a CDN is to serve content to end-users with high availability and high performance by caching frequently accessed content on servers that are closer to the end-users/
|
||||
|
||||
When a user requests content from a website that is using a CDN, the CDN will first check if the requested content is available in the cache of a nearby server. If the content is found in the cache, it is served to the user from the nearby server. If the content is not found in the cache, it is requested from the origin server (the original source of the content) and then cached on the nearby server for future requests.
|
||||
|
||||
|
||||
1
src/env.d.ts
vendored
@@ -1,4 +1,3 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useDebounceValue<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function usePagination(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
maxPagesToShow: number,
|
||||
) {
|
||||
return useMemo(() => {
|
||||
const pages: Array<number | string> = [];
|
||||
const half = Math.floor(maxPagesToShow / 2);
|
||||
const start = Math.max(1, currentPage - half);
|
||||
const end = Math.min(totalPages, currentPage + half);
|
||||
|
||||
if (start > 1) {
|
||||
pages.push(1);
|
||||
}
|
||||
|
||||
if (start > 2) {
|
||||
pages.push('more');
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (end < totalPages - 1) {
|
||||
pages.push('more');
|
||||
}
|
||||
|
||||
if (end < totalPages) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [currentPage, totalPages, maxPagesToShow]);
|
||||
}
|
||||