Compare commits

..

35 Commits

Author SHA1 Message Date
Arik Chakma
b1fad87eb6 feat: implement checklist 2024-03-28 02:52:40 +06:00
Kamran Ahmed
1d9adf742b Add Node.js questions link 2024-03-27 20:45:02 +00:00
Kamran Ahmed
625d9a1f5b fix: invalid link in backend performance best practices 2024-03-27 20:28:16 +00:00
Kamran Ahmed
08bc4c38ae fix: invalid link in backend performance best practices 2024-03-27 20:27:34 +00:00
Kamran Ahmed
cabbf51150 Make topic detail visible for logged in users 2024-03-27 20:25:53 +00:00
Arik Chakma
730af9b973 feat: ability to update email (#5370)
* chore: update email

* wip: verify email endpoint

* wip: implement success screen

* wip: social warning

* Update form for email update

* Update email form UI

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-03-27 15:38:47 +00:00
Vaibhav Chauhan
4e96a58e54 Add complexity video resource (#5399)
* feat/ Added complexity video resource

Added video resource for time and space complexity in Data structures and Algorithms section.

* Replace link

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-03-26 22:43:13 +00:00
Kamran Ahmed
5230ae22be Mark required fields 2024-03-26 15:54:08 +00:00
Arik Chakma
18deef46db feat: add increase generation limit options (#5388)
* wip: limit increase options

* feat: add increase AI limit options

* fix: overflow issue

* UI Updates

* UI for bypassing limits

* Refactor bypass limit

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-03-26 15:40:26 +00:00
Corey Barr
e6bea59ab5 Fix typo in Java Developer roadmap. (#5406) 2024-03-26 03:04:36 +00:00
Kamran Ahmed
66e4f3c97c Update twitter link 2024-03-26 03:02:39 +00:00
Kamran Ahmed
9511de967c Change backend performance dimensions 2024-03-25 21:53:25 +00:00
Kamran Ahmed
038bdb0e7d Add backend performance best practices 2024-03-25 21:34:47 +00:00
Kamran Ahmed
8a1f5d1bb4 Add backend performance best practices 2024-03-25 21:32:20 +00:00
Kamran Ahmed
3e8af3bd0f Add text-balance to guide headers 2024-03-25 21:27:30 +00:00
Kamran Ahmed
08a3970d08 Add content for backend performance best practices 2024-03-25 21:25:23 +00:00
Kamran Ahmed
e4f9b9fe01 Best practice content dir generation 2024-03-25 20:08:38 +00:00
Kamran Ahmed
2ab7690271 Best practice content dir generation 2024-03-25 19:40:53 +00:00
Kamran Ahmed
cdb9a87c85 Add backend performance best practices 2024-03-25 15:15:30 +00:00
Kamran Ahmed
0d43103323 Add "new" to new roadmaps on homepage 2024-03-25 15:11:25 +00:00
Kamran Ahmed
77d67e29eb fix: progress overflows the roadmap with 2024-03-25 15:11:05 +00:00
Aldiyar Dabarov
4a00a7bc79 Fix typo in System Design Roadmap (#5380) 2024-03-22 14:47:48 +06:00
Kamran Ahmed
ff2c13947b Add AI viewer behind signup 2024-03-21 20:34:48 +00:00
Kamran Ahmed
d880f84e0a Format result count 2024-03-20 16:22:49 +00:00
Kamran Ahmed
7cffcccfe8 Add result count on search 2024-03-20 16:13:28 +00:00
Kamran Ahmed
88ddeeb5fb Fix typescript errors 2024-03-20 15:47:30 +00:00
Kamran Ahmed
812a39154c feat: add search on the AI explore page (#5383)
* fix: type errors

* chore: implement roadmap pagination

* wip

* wip: merge conflicts

* wip: add search

* Add pagination

* Refactor AI search roadmaps

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-03-20 15:38:08 +00:00
Arik Chakma
6e6489bc4c feat: show pre-existing results on AI search input (#5349)
* feat: ai term suggestion input

* fix: add suggestion for roadmap

* Update spinner

* fix: hydration errors

* Refactor roadmap search and suggestions

* Remove limit from frontend

* Update roadmap title

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-03-19 20:42:29 +00:00
Kamran Ahmed
696e4f1890 Add missing og image 2024-03-19 12:27:29 +00:00
Kamran Ahmed
37c18ec3cc Update SEO text 2024-03-19 12:21:29 +00:00
Kamran Ahmed
9e3d50a84d Update FAQ text 2024-03-19 12:17:37 +00:00
Kamran Ahmed
01c65084b5 Add FAQ for backend roadmap 2024-03-19 12:16:49 +00:00
Kamran Ahmed
05fe51ca33 Add article backend developer tools 2024-03-19 12:08:18 +00:00
Kamran Ahmed
59c0a7d451 Add high res frontend og image 2024-03-19 03:44:49 +00:00
Arik Chakma
d0bd4d6faf feat: implement open graph (#5340)
* chore: add open graph images

* fix: open graph function

* fix: open graph query params

* fix: remove guide id

* fix: generate images on build time

* fix: external author image

* fix: special character issue

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-03-19 03:33:04 +00:00
107 changed files with 7078 additions and 533 deletions

5
.astro/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}

View File

@@ -29,7 +29,7 @@ export default defineConfig({
'mailto:',
'https://github.com/kamranahmedse',
'https://thenewstack.io',
'https://cs.fyi',
'https://kamranahmed.info',
'https://roadmap.sh',
];
if (whiteListedStarts.some((start) => href.startsWith(start))) {

View File

@@ -41,7 +41,7 @@
"image-size": "^1.1.1",
"jose": "^5.2.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.334.0",
"lucide-react": "^0.358.0",
"nanoid": "^5.0.5",
"nanostores": "^0.9.5",
"node-html-parser": "^6.1.12",

8
pnpm-lock.yaml generated
View File

@@ -60,8 +60,8 @@ dependencies:
specifier: ^3.0.5
version: 3.0.5
lucide-react:
specifier: ^0.334.0
version: 0.334.0(react@18.2.0)
specifier: ^0.358.0
version: 0.358.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.334.0(react@18.2.0):
resolution: {integrity: sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==}
/lucide-react@0.358.0(react@18.2.0):
resolution: {integrity: sha512-rBSptRjZTMBm24zsFhR6pK/NgbT18JegZGKcH4+1H3+UigMSRpeoWLtR/fAwMYwYnlJOZB+y8WpeHne9D6X6Kg==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -74,14 +74,16 @@ Here is the list of available roadmaps with more being actively worked upon.
There are also interactive best practices:
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
- [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)
- [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)
![](https://i.imgur.com/waxVImv.png)

View File

@@ -4,11 +4,7 @@ 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'
);
const BEST_PRACTICE_JSON_DIR = path.join(
__dirname,
'../public/jsons/best-practices'
'../src/data/best-practices',
);
const bestPracticeId = process.argv[2];
@@ -29,15 +25,14 @@ if (!allowedBestPracticeIds.includes(bestPracticeId)) {
const BEST_PRACTICE_CONTENT_DIR = path.join(
ALL_BEST_PRACTICES_DIR,
bestPracticeId,
'content'
'content',
);
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
const OpenAI = require('openai');
const openai = new OpenAI({
apiKey: OPEN_AI_API_KEY,
});
const openai = new OpenAIApi(configuration);
function getFilesInFolder(folderPath, fileList = {}) {
const files = fs.readdirSync(folderPath);
@@ -62,13 +57,19 @@ function getFilesInFolder(folderPath, fileList = {}) {
}
function writeTopicContent(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.`;
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}`;
console.log(`Generating '${topicTitle}'...`);
return new Promise((resolve, reject) => {
openai
.createChatCompletion({
openai.chat.completions
.create({
model: 'gpt-4',
messages: [
{
@@ -78,7 +79,7 @@ function writeTopicContent(topicTitle) {
],
})
.then((response) => {
const article = response.data.choices[0].message.content;
const article = response.choices[0].message.content;
resolve(article);
})
@@ -90,9 +91,12 @@ function writeTopicContent(topicTitle) {
async function writeFileForGroup(group, topicUrlToPathMapping) {
const topicId = group?.properties?.controlName;
const topicTitle = group?.children?.controls?.control?.find(
(control) => control?.typeID === 'Label'
)?.properties?.text;
const topicTitle = group?.children?.controls?.control
?.filter((control) => control?.typeID === 'Label')
.map((control) => control?.properties?.text)
.join(' ')
.toLowerCase();
const currTopicUrl = `/${topicId}`;
if (currTopicUrl.startsWith('/check:')) {
return;
@@ -102,7 +106,6 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
if (!contentFilePath) {
console.log(`Missing file for: ${currTopicUrl}`);
process.exit(0);
return;
}
@@ -123,8 +126,13 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
return;
}
if (!topicTitle) {
console.log(`Skipping ${topicId}. No title.`);
return;
}
const topicContent = await writeTopicContent(topicTitle);
newFileContent += `\n\n${topicContent}`;
newFileContent = `${topicContent}`;
console.log(`Writing ${topicId}..`);
fs.writeFileSync(contentFilePath, newFileContent, 'utf8');
@@ -138,14 +146,14 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
async function run() {
const topicUrlToPathMapping = getFilesInFolder(BEST_PRACTICE_CONTENT_DIR);
const bestPracticeJson = require(path.join(
BEST_PRACTICE_JSON_DIR,
`${bestPracticeId}.json`
));
const bestPracticeJson = require(
path.join(ALL_BEST_PRACTICES_DIR, `${bestPracticeId}/${bestPracticeId}`),
);
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) {

View File

@@ -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,10 +88,12 @@ function prepareDirTree(control, dirTree) {
return { dirTree };
}
const bestPractice = require(path.join(
__dirname,
`../public/jsons/best-practices/${bestPracticeId}`
));
const bestPractice = require(
path.join(
__dirname,
`../src/data/best-practices/${bestPracticeId}/${bestPracticeId}`,
),
);
const controls = bestPractice.mockup.controls.control;
// Prepare the dir tree that we will be creating

View File

@@ -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,6 +79,25 @@ 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
@@ -88,10 +107,10 @@ export function ActivityPage() {
/>
<div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
{learningRoadmaps.length === 0 &&
learningBestPractices.length === 0 && <EmptyActivity />}
{learningRoadmapsToShow.length === 0 &&
learningBestPracticesToShow.length === 0 && <EmptyActivity />}
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
{(learningRoadmapsToShow.length > 0 || learningBestPracticesToShow.length > 0) && (
<>
<h2 className="mb-3 text-xs uppercase text-gray-400">
Continue Following
@@ -104,26 +123,38 @@ export function ActivityPage() {
return updatedAtB.getTime() - updatedAtA.getTime();
})
.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('');
});
}}
/>
))}
.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('');
});
}}
/>
);
})}
{learningBestPractices
.sort((a, b) => {
@@ -132,6 +163,10 @@ export function ActivityPage() {
return updatedAtB.getTime() - updatedAtA.getTime();
})
.filter(
(bestPractice) =>
bestPractice.learning > 0 || bestPractice.done > 0,
)
.map((bestPractice) => (
<ResourceProgress
isCustomResource={bestPractice.isCustomResource}

View File

@@ -0,0 +1,82 @@
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>
);
}

View File

@@ -1,17 +1,11 @@
import { useEffect, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import {
type AppError,
type FetchError,
httpGet,
httpPost,
} from '../../lib/http';
import { type AppError, type FetchError, httpGet } 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 = [

View File

@@ -125,6 +125,32 @@ 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 && (
@@ -162,6 +188,8 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
onTopicAltClick={handleTopicAltClick}
onButtonNodeClick={handleLinkClick}
onLinkClick={handleLinkClick}
onChecklistCheckboxClick={handleChecklistCheckboxClick}
onChecklistLableClick={handleChecklistLabelClick}
fontFamily="Balsamiq Sans"
fontURL="/fonts/balsamiq.woff2"
/>

View File

@@ -0,0 +1,57 @@
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>
);
}

View File

@@ -0,0 +1,31 @@
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>
);
}

View File

@@ -1,9 +1,18 @@
import { useCallback, useEffect, useState } from 'react';
import { 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;
@@ -23,127 +32,154 @@ 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 [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],
);
const [pageState, setPageState] = useState<PageState>({
searchTerm: '',
sortBy: 'createdAt',
currentPage: 0,
});
useEffect(() => {
loadAIRoadmaps(currPage).finally(() => {
setIsLoading(false);
const queryParams = getUrlParams() as QueryParams;
setPageState({
searchTerm: queryParams.q || '',
sortBy: queryParams.s || 'createdAt',
currentPage: +(queryParams.p || '1'),
});
}, []);
const hasMorePages = currPage < totalPages;
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,
});
}}
/>
</>
);
return (
<section className="container mx-auto py-3 sm:py-6">
<div className="mb-6">
<AIRoadmapAlert isListing />
<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>
{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>
)}
{loadingIndicator}
{emptyRoadmaps}
{roadmapsList}
</section>
);
}

View File

@@ -0,0 +1,69 @@
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>
);
}

View File

@@ -0,0 +1,73 @@
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>
);
}

View File

@@ -0,0 +1,12 @@
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>
);
}

View File

@@ -48,10 +48,11 @@ 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='flex h-2 w-2'>
<span class='mr-1.5 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>
)
}

View File

@@ -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://twitter.com/kamrify'
href='https://kamranahmed.info'
target='_blank'
>
<span class='hidden sm:inline'>@kamrify</span>

View File

@@ -0,0 +1,284 @@
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>
);
}

View File

@@ -16,6 +16,7 @@ import {
getOpenAIKey,
isLoggedIn,
removeAuthToken,
setAIReferralCode,
visitAIRoadmap,
} from '../../lib/jwt';
import { RoadmapSearch } from './RoadmapSearch.tsx';
@@ -36,6 +37,10 @@ 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;
@@ -85,11 +90,15 @@ type GetAIRoadmapResponse = {
export function GenerateRoadmap() {
const roadmapContainerRef = useRef<HTMLDivElement>(null);
const { id: roadmapId } = getUrlParams() as { id: string };
const { id: roadmapId, rc: referralCode } = getUrlParams() as {
id: string;
rc?: 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] =
@@ -104,7 +113,7 @@ export function GenerateRoadmap() {
const [roadmapTopicLimitUsed, setRoadmapTopicLimitUsed] = useState(0);
const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAIKey();
const [openAPIKey, setOpenAPIKey] = useState<string | undefined>(getOpenAIKey());
const isKeyOnly = IS_KEY_ONLY_ROADMAP_GENERATION;
const isAuthenticatedUser = isLoggedIn();
@@ -120,12 +129,6 @@ 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);
@@ -171,10 +174,13 @@ 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: term,
title: roadmapTitle,
data: result,
});
}
@@ -193,11 +199,11 @@ export function GenerateRoadmap() {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!roadmapTerm) {
if (!roadmapTerm || isLoadingResults) {
return;
}
if (roadmapTerm === currentRoadmap?.topic) {
if (roadmapTerm === currentRoadmap?.term) {
return;
}
@@ -293,7 +299,8 @@ export function GenerateRoadmap() {
pageProgressMessage.set('Loading Roadmap');
const { response, error } = await httpGet<{
topic: string;
term: string;
title: string;
data: string;
}>(`${import.meta.env.PUBLIC_API_URL}/v1-get-ai-roadmap/${roadmapId}`);
@@ -313,7 +320,7 @@ export function GenerateRoadmap() {
data,
});
setRoadmapTerm(title);
setRoadmapTerm(term);
setGeneratedRoadmapContent(data);
visitAIRoadmap(roadmapId);
};
@@ -360,6 +367,17 @@ export function GenerateRoadmap() {
loadAIRoadmapLimit().finally(() => {});
}, []);
useEffect(() => {
if (!referralCode || isLoggedIn()) {
deleteUrlParam('rc');
return;
}
setAIReferralCode(referralCode);
deleteUrlParam('rc');
showLoginPopup();
}, []);
useEffect(() => {
if (!roadmapId || roadmapId === currentRoadmap?.id) {
return;
@@ -391,13 +409,13 @@ export function GenerateRoadmap() {
const pageUrl = `https://roadmap.sh/ai?id=${roadmapId}`;
const canGenerateMore = roadmapLimitUsed < roadmapLimit;
const isLoggedInUser = isLoggedIn();
return (
<>
{isConfiguring && (
<OpenAISettings
<IncreaseRoadmapLimit
onClose={() => {
setOpenAPIKey(getOpenAIKey());
setIsConfiguring(false);
loadAIRoadmapLimit().finally(() => null);
}}
@@ -479,17 +497,15 @@ export function GenerateRoadmap() {
>
{roadmapLimitUsed} of {roadmapLimit}
</span>{' '}
roadmaps generated.
roadmaps generated today.
</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"
>
By-pass all limits by{' '}
<span className="font-semibold">
adding your own OpenAI API key
</span>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
)}
@@ -516,15 +532,14 @@ export function GenerateRoadmap() {
onSubmit={handleSubmit}
className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center"
>
<input
type="text"
autoFocus
placeholder="e.g. Try searching for Ansible or DevOps"
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
<AITermSuggestionInput
value={roadmapTerm}
onInput={(e) =>
setRoadmapTerm((e.target as HTMLInputElement).value)
}
onValueChange={(value) => setRoadmapTerm(value)}
placeholder="e.g. Try searching for Ansible or DevOps"
wrapperClassName="grow"
onSelect={(id, title) => {
loadTermRoadmap(title).finally(() => null);
}}
/>
<button
type={'submit'}
@@ -539,37 +554,47 @@ export function GenerateRoadmap() {
}
}}
disabled={
isAuthenticatedUser &&
(!roadmapLimit ||
!roadmapTerm ||
roadmapLimitUsed >= roadmapLimit ||
roadmapTerm === currentRoadmap?.term ||
(isKeyOnly && !openAPIKey))
isLoadingResults ||
(isAuthenticatedUser &&
(!roadmapLimit ||
!roadmapTerm ||
roadmapLimitUsed >= roadmapLimit ||
roadmapTerm === currentRoadmap?.term ||
(isKeyOnly && !openAPIKey)))
}
>
{!isAuthenticatedUser && (
{isLoadingResults && (
<>
<Wand size={20} />
Generate
<span>Please wait..</span>
</>
)}
{isAuthenticatedUser && (
{!isLoadingResults && (
<>
{roadmapLimit > 0 && canGenerateMore && (
{!isAuthenticatedUser && (
<>
<Wand size={20} />
Generate
</>
)}
{roadmapLimit === 0 && <span>Please wait..</span>}
{isAuthenticatedUser && (
<>
{roadmapLimit > 0 && canGenerateMore && (
<>
<Wand size={20} />
Generate
</>
)}
{roadmapLimit > 0 && !canGenerateMore && (
<span className="flex items-center">
<Ban size={15} className="mr-2" />
Limit reached
</span>
{roadmapLimit === 0 && <span>Please wait..</span>}
{roadmapLimit > 0 && !canGenerateMore && (
<span className="flex items-center">
<Ban size={15} className="mr-2" />
Limit reached
</span>
)}
</>
)}
</>
)}
@@ -632,11 +657,48 @@ export function GenerateRoadmap() {
)}
</div>
<div
ref={roadmapContainerRef}
id="roadmap-container"
onClick={handleNodeClick}
className="relative px-4 py-5 [&>svg]:mx-auto [&>svg]:max-w-[1300px]"
/>
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>
</section>
</>
);

View File

@@ -0,0 +1,68 @@
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>
);
}

View File

@@ -5,13 +5,15 @@ 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 } = props;
const { onClose, onBack } = props;
const [defaultOpenAIKey, setDefaultOpenAIKey] = useState('');
@@ -28,141 +30,143 @@ export function OpenAISettings(props: OpenAISettingsProps) {
}, []);
return (
<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>
<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>
<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>
<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();
<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);
}}
/>
const normalizedKey = openaiApiKey.trim();
if (!normalizedKey) {
deleteOpenAIKey();
toast.success('OpenAI API key removed');
onClose();
return;
}
{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>
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');
{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 border border-red-500 px-4 py-2 text-sm text-red-600 transition-colors hover:bg-red-700 hover:text-white"
>
<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>
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>
);
}

View File

@@ -0,0 +1,164 @@
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>
);
}

View File

@@ -0,0 +1,50 @@
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>
</>
);
}

View File

@@ -0,0 +1,84 @@
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>
);
}

View File

@@ -1,17 +1,12 @@
import {
ArrowUpRight,
Ban,
CircleFadingPlus,
Cog,
Telescope,
Wand,
} from 'lucide-react';
import { ArrowUpRight, Ban, 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;
@@ -38,16 +33,23 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
const canGenerateMore = limitUsed < limit;
const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAIKey();
const isAuthenticatedUser = isLoggedIn();
const [openAPIKey, setOpenAPIKey] = useState('');
const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
const [isLoadingResults, setIsLoadingResults] = useState(false);
useEffect(() => {
setOpenAPIKey(getOpenAIKey() || '');
setIsAuthenticatedUser(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 && (
<OpenAISettings
<IncreaseRoadmapLimit
onClose={() => {
setOpenAPIKey(getOpenAIKey()!);
setIsConfiguring(false);
loadAIRoadmapLimit();
}}
@@ -78,15 +80,15 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
}}
className="flex w-full flex-col gap-2 sm:flex-row"
>
<input
autoFocus
type="text"
placeholder="Enter a topic to generate a roadmap for"
className="w-full rounded-md border border-gray-400 px-3 py-2.5 transition-colors focus:border-black focus:outline-none"
<AITermSuggestionInput
autoFocus={true}
value={roadmapTerm}
onInput={(e) =>
setRoadmapTerm((e.target as HTMLInputElement).value)
}
onValueChange={(value) => setRoadmapTerm(value)}
placeholder="Enter a topic to generate a roadmap for"
wrapperClassName="w-full"
onSelect={(roadmapId, roadmapTitle) => {
onLoadTerm(roadmapTitle);
}}
/>
<button
className={cn(
@@ -100,33 +102,44 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
}
}}
disabled={
isAuthenticatedUser &&
(!limit ||
!roadmapTerm ||
limitUsed >= limit ||
(isKeyOnly && !openAPIKey))
isLoadingResults ||
(isAuthenticatedUser &&
(!limit ||
!roadmapTerm ||
limitUsed >= limit ||
(isKeyOnly && !openAPIKey)))
}
>
{!isAuthenticatedUser && (
{isLoadingResults && (
<>
<Wand size={20} />
Generate
<span>Please wait..</span>
</>
)}
{isAuthenticatedUser && (
{!isLoadingResults && (
<>
{(!limit || canGenerateMore) && (
{!isAuthenticatedUser && (
<>
<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>
)}
</>
)}
</>
)}
@@ -242,7 +255,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
>
{limitUsed} of {limit}
</span>{' '}
roadmaps.
roadmaps today.
</p>
{isAuthenticatedUser && (
<p className="flex items-center text-sm">
@@ -251,10 +264,8 @@ 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"
>
By-pass all limits by{' '}
<span className="font-semibold">
adding your own OpenAI API key
</span>
Need to generate more?{' '}
<span className="font-semibold">Click here.</span>
</button>
)}

View File

@@ -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, FileText, X } from 'lucide-react';
import {Ban, Cog, Contact, FileText, User, UserRound, X} from 'lucide-react';
import { Spinner } from '../ReactIcons/Spinner';
import type { RoadmapNodeDetails } from './GenerateRoadmap';
import { getOpenAIKey, isLoggedIn, removeAuthToken } from '../../lib/jwt';
@@ -43,12 +43,10 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
const generateAiRoadmapTopicContent = async () => {
setIsLoading(true);
setError('');
//
// if (topicLimitUsed >= topicLimit) {
// setError('Maximum limit reached');
// setIsLoading(false);
// return;
// }
if (!isLoggedIn()) {
return;
}
if (!roadmapId || !nodeTitle) {
setIsLoading(false);
@@ -133,50 +131,44 @@ 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"
>
<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() && (
<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>
)}
{isLoading && (
{isLoggedIn() && isLoading && (
<div className="mt-6 flex w-full justify-center">
<Spinner
outerFill="#d1d5db"
@@ -186,6 +178,22 @@ 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">

View File

@@ -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'>
<h1 class='my-0 text-2xl font-bold sm:my-3.5 sm:text-5xl text-balance'>
{frontmatter.title}
</h1>
<p class='hidden text-xl text-gray-400 sm:block'>

View File

@@ -6,12 +6,19 @@ 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 } = props;
const {
onClose,
children,
bodyClassName,
wrapperClassName,
overlayClassName,
} = props;
const popupBodyEl = useRef<HTMLDivElement>(null);
@@ -24,18 +31,23 @@ export function Modal(props: ModalProps) {
});
return (
<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(
'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={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}

View File

@@ -0,0 +1,101 @@
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"
>
&larr;
</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);
}}
>
&rarr;
</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>
);
}

View File

@@ -0,0 +1,57 @@
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(() => {});
}}
/>
</>
);
}

View File

@@ -0,0 +1,245 @@
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>
</>
);
}

View File

@@ -1,26 +1,28 @@
import { type FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
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();
export default function UpdatePasswordForm() {
const [authProvider, setAuthProvider] = useState('');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPasswordConfirmation, setNewPasswordConfirmation] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');
setSuccess('');
if (newPassword !== newPasswordConfirmation) {
setError('Passwords do not match');
toast.error('Passwords do not match');
setIsLoading(false);
return;
@@ -32,50 +34,26 @@ export default function UpdatePasswordForm() {
oldPassword: authProvider === 'email' ? currentPassword : 'social-auth',
password: newPassword,
confirmPassword: newPasswordConfirmation,
}
);
if (error) {
setError(error.message || 'Something went wrong');
setIsLoading(false);
return;
}
setError('');
setCurrentPassword('');
setNewPassword('');
setNewPasswordConfirmation('');
setSuccess('Password updated successfully');
setIsLoading(false);
};
const loadProfile = async () => {
setIsLoading(true);
const { error, response } = await httpGet(
`${import.meta.env.PUBLIC_API_URL}/v1-me`
},
);
if (error || !response) {
toast.error(error?.message || 'Something went wrong');
setIsLoading(false);
setError(error?.message || 'Something went wrong');
return;
}
const { authProvider } = response;
setAuthProvider(authProvider);
setCurrentPassword('');
setNewPassword('');
setNewPasswordConfirmation('');
toast.success('Password updated successfully');
setIsLoading(false);
setTimeout(() => {
window.location.reload();
}, 1000);
};
useEffect(() => {
loadProfile().finally(() => {
pageProgressMessage.set('');
});
}, []);
return (
<form onSubmit={handleSubmit}>
<div className="mb-8 hidden md:block">
@@ -98,7 +76,7 @@ export default function UpdatePasswordForm() {
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}
@@ -122,7 +100,7 @@ export default function UpdatePasswordForm() {
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}
@@ -145,7 +123,7 @@ export default function UpdatePasswordForm() {
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"
@@ -156,19 +134,11 @@ export default function UpdatePasswordForm() {
/>
</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}
disabled={
isLoading || !newPassword || newPassword !== newPasswordConfirmation
}
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'}

View File

@@ -30,7 +30,7 @@ export function UpdateProfileForm() {
linkedin: linkedin || undefined,
twitter: twitter || undefined,
website: website || undefined,
}
},
);
if (error || !response) {
@@ -45,11 +45,10 @@ 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) {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
---
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'
---

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1 @@
#

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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 systems 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 users 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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 systems 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -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: true
isNew: false
type: 'textual'
date: 2024-02-27
sitemap:

View File

@@ -0,0 +1,393 @@
---
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, were not just writing code on a text editor without any other external help. Whether we realize it or not, were constantly using different development tools to improve the way we work and the speed at which we can deliver our code.
In this article, well 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 well tackle are:
- IDEs and Editors
- Database Tools
- Collaboration
- Hosting Services
- API-Related Tools
- Productivity
So lets 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 youll 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 were focusing on right now).
Lets 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).
![Jetbrains](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849700950.png)
The benefit of using these IDEs, in comparison with others, is that given how theyre 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 theyre not all free. While not all of them are priced the same, I recommend you check out your IDEs 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 IDEs strongest selling point is that its incredibly extensible through plugins. And the community using it is so big and varied that there are plugins for literally anything you need.
![VSCode](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849510314.png)
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 dont 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](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849771500.png)
Zed, like VSCode, is an all-purpose code editor, letting you code in whatever language you want (whether youre doing web development or not). The main reasons why youd 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, its 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 youll want to keep reading.
![Sublime](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849579321.png)
With a refreshed UI (if youve used Sublime Text in the past, youll 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](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849488670.png)
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 Vims 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, its really hard to go back.
Just like with all the generic IDEs here, youll 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. Theyre a ubiquitous backend tool in the realm of web development.
Lets 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 youre 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.
![DataGrip](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849834698.png)
While the pricing of these tools might be a bit steep (especially if you go with the IDE as well), its definitely a solid option if youre 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.
![Navicat](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849810856.png)
The free trial for Navicat only lasts 14 days, and then youll 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 youd 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 dont mind their price, that is.
### 8. TablePlus
[Tableplus](https://tableplus.com/) is very similar to Navicat in the sense that its 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.
![TablePlus](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849788403.png)
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/).
![DBeaver Demo](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849342173.png)
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 youre working as a solo-dev (and even then!), collaboration tools allow you to coordinate your work and understand whos 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.
![Trello](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849443769.png)
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, Mondays plans are accessible, especially when compared to other alternatives.
![Monday](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849749004.png)
Mondays 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](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849933042.png)
Basecamps 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, its perfect for understanding the basic way of using Wrike and how useful it can be to you in your current project.
![Wrike](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849645662.png)
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 youre 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, youll 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).
Lets 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 clicks distance. Were 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](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849461849.png)
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.
![Heroku](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849852086.png)
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, youll 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 everythings already managed.
![Digital Ocean](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849911479.png)
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 youre looking to host your website or app and you dont want to have to deal with server maintenance and configuration, then this is a good option for you.
![Hetzner](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849893225.png)
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 youre building a NextJS application and youre 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.
![Vercel](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849618859.png)
And since theyre 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 youd expect from a PaaS provider.
While its true they dont 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.
Youre 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](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849533040.png)
Render also offers managed MySQL and managed Redis instances for you to use, even in their free tier. In the end, unless youve been using Heroku for a while and youre 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.
![OVHCloud](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849730064.png)
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, well always be dealing with APIs (Application Programming Interface), either through using the ones created by others or writing our own.
Whatever youre doing, its always good to have some backend tools to help you build and test them faster, so lets 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.
Thats where [Swagger](https://swagger.io/) comes into play.
![Swagger](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849662815.png)
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 companys 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.
![Postman](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849422682.png)
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 werent 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 youre a macOS user, then [iTerm](https://iterm2.com/) is definitely one of the “must haves” you need to look into. As a backend developer, youll spend a lot of your day in the terminal.
![iTerm](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849558918.png)
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 youre 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 youre spending several hours typing commands in your terminal. For example, you get features such as:
- Advanced tab auto-complete
- Extensibility
- Spelling corrections
- And more.
![Oh my Zsh](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849870732.png)
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.
![Raycast](https://assets.roadmap.sh/guest/6529303b545cb53d4aa730ca_1710849400409.png)
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.

View File

@@ -32,5 +32,12 @@ 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 youll 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, its up to you to try them and pick the ones that work best for you.',
]
}
];
---

View File

@@ -1,3 +1,5 @@
# 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.
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)

View File

@@ -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: true
isNew: false
dimensions:
width: 968
height: 1814.72

View File

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

View File

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

@@ -1,3 +1,4 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
interface ImportMetaEnv {

17
src/hooks/use-debounce.ts Normal file
View File

@@ -0,0 +1,17 @@
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;
}

View File

@@ -0,0 +1,36 @@
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]);
}

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