Compare commits

...

35 Commits

Author SHA1 Message Date
Arik Chakma
27d2bcb72d Fix editor link 2023-10-16 22:36:30 +06:00
Arik Chakma
6b395de616 Add share with others button 2023-10-16 21:28:28 +06:00
Arik Chakma
06d9b3971a Add Edit button in the roadmap list 2023-10-16 20:30:32 +06:00
Kamran Ahmed
b06e82de5f Sponsor for nginx 2023-10-13 22:41:46 +01:00
Kamran Ahmed
d65ecac777 Account dropdown changes 2023-10-13 19:52:21 +01:00
Kamran Ahmed
c46d962803 Add links to questions 2023-10-12 21:22:01 +01:00
Kamran Ahmed
bd4e7ea3d0 Add links to questions 2023-10-12 21:20:21 +01:00
Kamran Ahmed
252b083a48 add roadmap editor image 2023-10-12 20:56:01 +01:00
Arik Chakma
abbeb717d1 Add JavaScript questions (#4505)
* Add Javascript questions

* wip: add more questions

* wip: add ternary operator

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* fix: set example

* wip: add more questions

* wip: add more question

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add another question

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions
2023-10-12 15:03:19 +01:00
Kamran Ahmed
485ca9dd8f Spring testing link fix 2023-10-11 14:32:40 +01:00
Kamran Ahmed
c3315fb41e Fix typo on teams page 2023-10-11 12:56:56 +01:00
Kamran Ahmed
6ed436674f Discovery page option in sharing 2023-10-10 00:12:05 +01:00
Kamran Ahmed
76c6c4dc1f isDiscoverable not persisted 2023-10-10 00:06:24 +01:00
Kamran Ahmed
cb56e85651 Discoverable option selection 2023-10-09 23:27:49 +01:00
Kamran Ahmed
dcf740e275 Update share buttons text 2023-10-09 21:57:33 +01:00
Arik Chakma
16662ed699 Implement Social Share options (#4569)
* Implement social share options

* Minor fix
2023-10-09 21:49:21 +01:00
Kamran Ahmed
6f9fe361ae Change style of custom roadmap page 2023-10-09 09:07:59 +01:00
Arik Chakma
036b34c6f3 Implement Custom Roadmap minor features (#4565)
* Remove roadmap type

* Add Edit Roadmap button

* Add Edit Roadmap permission

* Add Edit and Share roadmap button

* Remove Margin

* Implement Discoverable Checkbox

* Add Loading State for buttons
2023-10-09 08:44:30 +01:00
Kamran Ahmed
93c2043f23 Fix warning in hero roadmap 2023-10-08 18:38:02 +01:00
Saleh Hashemi
d2da3c8621 update checkout version to v4 (#4559) 2023-10-07 22:15:01 +01:00
Kamran Ahmed
4aa8f15c07 Add email icon in footer 2023-10-07 15:31:24 +01:00
Arik Chakma
ceb4c3b95d Remove invited members from sharing settings (#4555)
* Fix team member list

* Minor change
2023-10-06 19:00:48 +01:00
Kamran Ahmed
7ec5e30b51 Hero roadmap section updates 2023-10-06 19:00:08 +01:00
Kamran Ahmed
e5e0a7c8c5 Add teams banner 2023-10-04 20:32:28 +01:00
Kamran Ahmed
90f3ffe270 Add banner for teams 2023-10-04 16:13:41 +01:00
Kamran Ahmed
ce47a7433e Teams button in navigation 2023-10-04 15:44:34 +01:00
Selva Muthu Kumaran
21b8358683 roadmap-aspnet-change-tracker-api.md (#4546)
aspnet-change-tracker-api URL fixed
fixes : #4544
2023-10-04 20:40:45 +06:00
Kamran Ahmed
e1751b105f Add team page 2023-10-04 15:28:46 +01:00
Kamran Ahmed
e43bea7c40 Setup redirects on the teams page 2023-10-04 15:22:20 +01:00
Kamran Ahmed
5fa669aec2 Update team page 2023-10-04 15:06:59 +01:00
Kamran Ahmed
4b8f868b2b Add roadmaps and friends to account dropdown 2023-10-04 10:34:29 +01:00
Kamran Ahmed
a0743a8272 Fix sharing options button 2023-10-04 10:30:28 +01:00
Arik Chakma
2cae13c090 Add Members while Transferring Roadmap (#4534)
* Add members while Transferring Roadmap

* Implement Responsive in Roadmaps page
2023-10-04 10:15:56 +01:00
Kamran Ahmed
0bf287f1d6 Add features to pricing 2023-10-04 10:12:08 +01:00
Kamran Ahmed
d7d819b4b3 Add teams introduction page 2023-10-03 21:07:53 +01:00
113 changed files with 2710 additions and 278 deletions

View File

@@ -12,7 +12,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v1

View File

@@ -9,7 +9,7 @@ jobs:
upgrade-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 18

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

View File

@@ -24,7 +24,7 @@
Roadmaps are now interactive, you can click the nodes to read more about the topics.
### [View all Roadmaps](https://roadmap.sh)
### [View all Roadmaps](https://roadmap.sh)  ·  [Best Practices](https://roadmap.sh/best-practices)  ·  [Questions](https://roadmap.sh/questions)
![](https://i.imgur.com/waxVImv.png)
@@ -67,13 +67,18 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Docker Roadmap](https://roadmap.sh/docker)
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
We have also added a new form of visual content covering best practices:
There are also interactive best practices:
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
..and questions to help you test, rate and improve your knowledge
- [JavaScript Questions](https://roadmap.sh/questions/javascript)
- [React Questions](https://roadmap.sh/questions/react)
![](https://i.imgur.com/waxVImv.png)
## Share with the community

View File

@@ -56,6 +56,12 @@ export function GitHubButton(props: GitHubButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GITHUB_REDIRECT_AT);
localStorage.removeItem(GITHUB_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {

View File

@@ -55,6 +55,12 @@ export function GoogleButton(props: GoogleButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GOOGLE_REDIRECT_AT);
localStorage.removeItem(GOOGLE_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
@@ -86,10 +92,11 @@ export function GoogleButton(props: GoogleButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath =
['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GOOGLE_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GOOGLE_LAST_PAGE, pagePath);

View File

@@ -55,6 +55,12 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
localStorage.removeItem(LINKEDIN_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {

View File

@@ -73,7 +73,10 @@ function handleAuthenticated() {
// If the user is on a guest route, redirect them to the home page
if (guestRoutes.includes(window.location.pathname)) {
window.location.href = '/';
const authRedirect = window.localStorage.getItem('authRedirect') || '/';
window.localStorage.removeItem('authRedirect');
window.location.href = authRedirect;
}
}

View File

@@ -40,6 +40,14 @@ const defaultPages: PageType[] = [
icon: GroupIcon.src,
isProtected: true,
},
{
id: 'friends',
url: '/account/friends',
title: 'Friends',
group: 'Pages',
icon: GroupIcon.src,
isProtected: true,
},
{
id: 'roadmaps',
url: '/roadmaps',
@@ -47,6 +55,14 @@ const defaultPages: PageType[] = [
group: 'Pages',
icon: RoadmapIcon.src,
},
{
id: 'account-roadmaps',
url: '/account/roadmaps',
title: 'Custom Roadmaps',
group: 'Pages',
icon: RoadmapIcon.src,
isProtected: true,
},
{
id: 'best-practices',
url: '/best-practices',

View File

@@ -15,6 +15,7 @@ import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
resourceId: string;
resourceType: string;

View File

@@ -2,20 +2,17 @@ import { Plus } from 'lucide-react';
import { isLoggedIn } from '../../../lib/jwt';
import { showLoginPopup } from '../../../lib/popup';
import { cn } from '../../../lib/classname';
import {
type AllowedCustomRoadmapType,
type AllowedRoadmapVisibility,
CreateRoadmapModal,
} from './CreateRoadmapModal';
import { CreateRoadmapModal } from './CreateRoadmapModal';
import { useState } from 'react';
type CreateRoadmapButtonProps = {
className?: string;
type?: AllowedCustomRoadmapType;
text?: string;
teamId?: string;
};
export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
const { className, type } = props;
const { teamId, className, text = 'Create your own Roadmap' } = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
@@ -31,7 +28,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
<>
{isCreatingRoadmap && (
<CreateRoadmapModal
type={type}
teamId={teamId}
onClose={() => {
setIsCreatingRoadmap(false);
}}
@@ -46,7 +43,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
onClick={toggleCreateRoadmapHandler}
>
<Plus size={16} />
Create your own Roadmap
{text}
</button>
</>
);

View File

@@ -10,7 +10,6 @@ import { Modal } from '../../Modal';
import { useToast } from '../../../hooks/use-toast';
import { httpPost } from '../../../lib/http';
import { cn } from '../../../lib/classname';
import { allowedVisibilityLabels } from '../ShareRoadmapModal';
export const allowedRoadmapVisibility = [
'me',
@@ -30,6 +29,7 @@ export interface RoadmapDocument {
description?: string;
creatorId: string;
teamId?: string;
isDiscoverable: boolean;
type: AllowedCustomRoadmapType;
visibility: AllowedRoadmapVisibility;
sharedFriendIds?: string[];
@@ -46,12 +46,11 @@ interface CreateRoadmapModalProps {
onClose: () => void;
onCreated?: (roadmap: RoadmapDocument) => void;
teamId?: string;
type?: AllowedCustomRoadmapType;
visibility?: AllowedRoadmapVisibility;
}
export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
const { onClose, onCreated, teamId, type: defaultType = 'role' } = props;
const { onClose, onCreated, teamId } = props;
const titleRef = useRef<HTMLInputElement>(null);
const toast = useToast();
@@ -59,7 +58,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
const [isLoading, setIsLoading] = useState(false);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [type, setType] = useState<AllowedCustomRoadmapType>(defaultType);
const isInvalidDescription = description?.trim().length > 80;
async function handleSubmit(
@@ -71,7 +69,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
return;
}
if (title.trim() === '' || isInvalidDescription || !type) {
if (title.trim() === '' || isInvalidDescription) {
toast.error('Please fill all the fields');
return;
}
@@ -82,7 +80,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
{
title,
description,
type,
...(teamId && {
teamId,
}),
@@ -114,7 +111,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
setTitle('');
setDescription('');
setType('role');
setIsLoading(false);
}
@@ -149,7 +145,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title"
id="title"
required
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
className="block text-black w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
placeholder="Enter Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
@@ -169,7 +165,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description"
required
className={cn(
'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
'block text-black h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
isInvalidDescription && 'border-red-300 bg-red-100'
)}
placeholder="Enter Description"
@@ -182,33 +178,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
</div>
</div>
<div className="mt-4">
<label
htmlFor="type"
className="block text-xs uppercase text-gray-400"
>
Type
</label>
<div className="mt-1">
<select
id="type"
name="type"
required
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
value={type}
onChange={(e) =>
setType(e.target.value as AllowedCustomRoadmapType)
}
>
{allowedCustomRoadmapType.map((type) => (
<option key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)} Based Roadmap
</option>
))}
</select>
</div>
</div>
<div
className={cn('mt-4 flex justify-between gap-2', teamId && 'mt-8')}
>

View File

@@ -1,11 +1,29 @@
import { CircleSlash } from 'lucide-react';
import {CircleSlash, PenSquare, Shapes} from 'lucide-react';
type EmptyRoadmapProps = {
roadmapId: string;
canManage: boolean;
};
export function EmptyRoadmap(props: EmptyRoadmapProps) {
const { roadmapId, canManage } = props;
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
export function EmptyRoadmap() {
return (
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center">
<CircleSlash className="mx-auto h-20 w-20 text-gray-400" />
<h3 className="mt-4">This roadmap is currently empty.</h3>
<h3 className="mt-2">This roadmap is currently empty.</h3>
{canManage && (
<a
href={editUrl}
className="mt-4 rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600 flex items-center"
>
<Shapes className="inline-block mr-2 h-4 w-4" />
Edit Roadmap
</a>
)}
</div>
</div>
);

View File

@@ -7,6 +7,7 @@ import {
Globe,
LockIcon,
Users,
PenSquare,
} from 'lucide-react';
import { useToast } from '../../hooks/use-toast';
import {
@@ -60,6 +61,8 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
sharedFriendIds={selectedRoadmap.sharedFriendIds}
sharedTeamMemberIds={selectedRoadmap.sharedTeamMemberIds}
@@ -140,7 +143,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
return (
<li
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_172px]"
key={roadmap._id!}
>
<div className="mb-3 grid grid-cols-1 sm:mb-0">
@@ -182,6 +185,16 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
<ExternalLink className="inline-block h-4 w-4" />
Visit
</a>
<a
href={editorLink}
className={
'ml-2 flex items-center gap-2 rounded-md border border-gray-800 bg-gray-900 px-2.5 py-1.5 text-xs text-white hover:bg-gray-800 focus:outline-none'
}
target={'_blank'}
>
<PenSquare className="inline-block h-4 w-4" />
Edit
</a>
</div>
</li>
);

View File

@@ -24,6 +24,8 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}

View File

@@ -8,6 +8,9 @@ import { httpDelete, httpPut } from '../../lib/http';
import { type TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { useToast } from '../../hooks/use-toast';
import { RoadmapActionButton } from './RoadmapActionButton';
import { Lock, Shapes } from 'lucide-react';
import { Modal } from '../Modal';
import { ShareSuccess } from '../ShareOptions/ShareSuccess';
type RoadmapHeaderProps = {};
@@ -21,9 +24,11 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
_id: roadmapId,
creator,
team,
visibility,
} = useStore(currentRoadmap) || {};
const [isSharing, setIsSharing] = useState(false);
const [isSharingWithOthers, setIsSharingWithOthers] = useState(false);
const toast = useToast();
async function deleteResource() {
@@ -64,6 +69,22 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${creator?.avatar}`
: '/images/default-avatar.png';
const sharingWithOthersModal = isSharingWithOthers && (
<Modal
onClose={() => setIsSharingWithOthers(false)}
wrapperClassName="max-w-lg"
bodyClassName="p-4 flex flex-col"
>
<ShareSuccess
visibility="public"
roadmapId={roadmapId!}
description={description}
onClose={() => setIsSharingWithOthers(false)}
isSharingWithOthers={true}
/>
</Modal>
);
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
@@ -116,52 +137,78 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
<span className="ml-2">Subscribe</span>
</button>
</div>
{$canManageCurrentRoadmap && (
<div className="flex items-center gap-2">
{isSharing && $currentRoadmap && (
<ShareOptionsModal
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
sharedTeamMemberIds={
$currentRoadmap?.sharedTeamMemberIds || []
}
onClose={() => setIsSharing(false)}
onShareSettingsUpdate={(settings) => {
currentRoadmap.set({
...$currentRoadmap,
...settings,
});
<div className="flex items-center gap-2">
{$canManageCurrentRoadmap && (
<>
{isSharing && $currentRoadmap && (
<ShareOptionsModal
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
sharedTeamMemberIds={
$currentRoadmap?.sharedTeamMemberIds || []
}
onClose={() => setIsSharing(false)}
onShareSettingsUpdate={(settings) => {
currentRoadmap.set({
...$currentRoadmap,
...settings,
});
}}
/>
)}
<a
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
$currentRoadmap?._id
}`}
target="_blank"
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Shapes className="mr-1.5 h-4 w-4 stroke-[2.5]" />
<span className="hidden sm:inline-block">Edit Roadmap</span>
<span className="sm:hidden">Edit</span>
</a>
<button
onClick={() => setIsSharing(true)}
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
Sharing
</button>
<RoadmapActionButton
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?'
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
/>
)}
</>
)}
<RoadmapActionButton
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?'
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
onCustomize={() => {
const editorLink = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${$currentRoadmap?._id}`;
window.open(editorLink, '_blank');
}}
onUpdateSharing={() => {
setIsSharing(true);
}}
/>
</div>
)}
{!$canManageCurrentRoadmap && visibility === 'public' && (
<>
{sharingWithOthersModal}
<button
onClick={() => setIsSharingWithOthers(true)}
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
Share with Others
</button>
</>
)}
</div>
</div>
<RoadmapHint

View File

@@ -86,13 +86,13 @@ export function RoadmapListPage() {
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
)}
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="mb-6 flex flex-col justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
<div className="flex grow items-center gap-2">
{tabTypes.map((tab) => {
return (
<button
key={tab.value}
className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${
className={`relative flex w-full items-center justify-center whitespace-nowrap rounded-md border p-1 px-3 text-sm sm:w-auto ${
activeTab === tab.value ? ' border-gray-400 bg-gray-200 ' : ''
} w-full sm:w-auto`}
onClick={() => setActiveTab(tab.value)}

View File

@@ -12,8 +12,6 @@ import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { EmptyRoadmap } from './EmptyRoadmap';
import { isLoggedIn } from '../../lib/jwt';
import { httpPost } from '../../lib/http';
type RoadmapRendererProps = {
roadmap: RoadmapDocument;
@@ -170,7 +168,9 @@ export function RoadmapRenderer(props: RoadmapRendererProps) {
});
}}
/>
{hideRenderer && <EmptyRoadmap />}
{hideRenderer && (
<EmptyRoadmap roadmapId={roadmapId} canManage={roadmap.canManage} />
)}
</div>
</div>
);

View File

@@ -13,7 +13,11 @@ export function SkeletonRoadmapHeader() {
<div className="flex justify-between gap-2 sm:gap-0">
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
<div className="flex items-center gap-2">
<div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139.71px]" />
<div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
</div>
</div>
<div className="mb-0 mt-4 rounded-md border-0 sm:-mb-[65px] sm:mt-7 sm:border">

View File

@@ -42,13 +42,7 @@ const {
{
showCreateRoadmap && (
<li>
<CreateRoadmapButton
client:load
className='min-h-[54px]'
type={
heading.toLowerCase().indexOf('role') > -1 ? 'role' : 'skill'
}
/>
<CreateRoadmapButton client:load className='min-h-[54px]' />
</li>
)
}

View File

@@ -31,7 +31,8 @@ import Icon from './AstroIcon.astro';
<a
class='px-2 py-1.5 transition-colors hover:text-white sm:border-b-0 sm:px-0 sm:py-0'
href='https://youtube.com/theroadmap?sub_confirmation=1'
target='_blank'>YouTube</a>
target='_blank'>YouTube</a
>
</p>
<div class='flex flex-col justify-between gap-12 sm:flex-row'>
@@ -67,20 +68,30 @@ import Icon from './AstroIcon.astro';
<a href='/privacy' class='hover:text-white'>Privacy</a>
<span class='mx-1.5'>&middot;</span>
<a
aria-label="Subscribe to YouTube channel"
aria-label='Write us an email'
href='mailto:info@roadmap.sh'
class='hover:text-white'
>
<AstroIcon icon='letter' class='inline-block h-5 w-5' />
</a>
<a
aria-label='Subscribe to YouTube channel'
href='https://youtube.com/theroadmap?sub_confirmation=1'
target='_blank'
class='hover:text-white'
class='ml-2 hover:text-white'
>
<AstroIcon icon='youtube' class='inline-block h-5 w-5' />
</a>
<a
aria-label="Follow on Twitter"
aria-label='Follow on Twitter'
href='https://twitter.com/roadmapsh'
target='_blank'
class='ml-2 hover:text-white'
>
<AstroIcon icon='twitter-fill' class='inline-block h-5 w-5 fill-current' />
<AstroIcon
icon='twitter-fill'
class='inline-block h-5 w-5 fill-current'
/>
</a>
</p>
</div>

View File

@@ -1,23 +1,30 @@
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { TeamAnnouncement } from '../TeamAnnouncement';
type EmptyProgressProps = {
title?: string;
message?: string;
title?: string;
message?: string;
};
export function EmptyProgress(props: EmptyProgressProps) {
const {
title = 'Start learning ..',
message = 'Your progress and favorite roadmaps will show up here.',
} = props;
const {
title = 'Start learning ..',
message = 'Your progress and favorite roadmaps will show up here.',
} = props;
return (
<div className="relative flex min-h-full flex-col items-start sm:items-center justify-center py-6">
<h2 className={'mb-1 flex items-center text-lg sm:text-2xl text-gray-200'}>
<CheckIcon additionalClasses='mr-2 top-[0.5px] w-[16px] h-[16px] sm:w-[20px] sm:h-[20px]' />
Start learning ..
</h2>
<p className={'text-gray-400 text-sm sm:text-base'}>{message}</p>
</div>
);
return (
<div className="relative flex min-h-full flex-col items-start justify-center py-6 sm:items-center">
<h2
className={'mb-1.5 flex items-center text-lg text-gray-200 sm:text-2xl'}
>
<CheckIcon additionalClasses="mr-2 top-[0.5px] w-[16px] h-[16px] sm:w-[20px] sm:h-[20px]" />
{title}
</h2>
<p className={'text-sm text-gray-400 sm:text-base'}>{message}</p>
<p className="mt-5">
<TeamAnnouncement />
</p>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { EmptyProgress } from './EmptyProgress';
import { httpGet } from '../../lib/http';
import { HeroRoadmaps } from './HeroRoadmaps';
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
import { isLoggedIn } from '../../lib/jwt';
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
export type UserProgressResponse = {
resourceId: string;
@@ -15,6 +16,11 @@ export type UserProgressResponse = {
total: number;
updatedAt: Date;
isCustomResource: boolean;
team?: {
name: string;
id: string;
role: AllowedMemberRoles;
};
}[];
function renderProgress(progressList: UserProgressResponse) {
@@ -114,25 +120,43 @@ export function FavoriteRoadmaps() {
}
const hasProgress = progress?.length > 0;
const customRoadmaps = progress?.filter((p) => p.isCustomResource);
const customRoadmaps = progress?.filter(
(p) => p.isCustomResource && !p.team?.name
);
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
const teamRoadmaps: HeroTeamRoadmaps = progress
?.filter((p) => p.isCustomResource && p.team?.name)
.reduce((acc: HeroTeamRoadmaps, curr) => {
const currTeam = curr.team!;
if (!acc[currTeam.name]) {
acc[currTeam.name] = [];
}
acc[currTeam.name].push(curr);
return acc;
}, {});
return (
<div
className={`flex min-h-[192px] bg-gradient-to-b transition-opacity duration-500 sm:min-h-[280px] opacity-${containerOpacity} ${
hasProgress && `border-t border-t-[#1e293c]`
}`}
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
>
<div className="container min-h-full">
{!isLoading && progress?.length == 0 && <EmptyProgress />}
{hasProgress && (
<HeroRoadmaps
showCustomRoadmaps={true}
customRoadmaps={customRoadmaps}
progress={defaultRoadmaps}
isLoading={isLoading}
/>
)}
<div
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
hasProgress && `border-t border-t-[#1e293c]`
}`}
>
<div className="container min-h-full">
{!isLoading && progress?.length == 0 && <EmptyProgress />}
{hasProgress && (
<HeroRoadmaps
teamRoadmaps={teamRoadmaps}
customRoadmaps={customRoadmaps}
progress={defaultRoadmaps}
isLoading={isLoading}
/>
)}
</div>
</div>
</div>
);

View File

@@ -3,10 +3,11 @@ import { CheckIcon } from '../ReactIcons/CheckIcon';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { Spinner } from '../ReactIcons/Spinner';
import type { ResourceType } from '../../lib/resource-progress';
import { MapIcon } from 'lucide-react';
import { MapIcon, Users2 } from 'lucide-react';
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { useState } from 'react';
import { type ReactNode, useState } from 'react';
import { TeamAnnouncement } from '../TeamAnnouncement';
type ProgressRoadmapProps = {
url: string;
@@ -55,7 +56,7 @@ function HeroRoadmap(props: ProgressRoadmapProps) {
type ProgressTitleProps = {
icon: any;
isLoading?: boolean;
title: string;
title: string | ReactNode;
};
export function HeroTitle(props: ProgressTitleProps) {
@@ -73,22 +74,39 @@ export function HeroTitle(props: ProgressTitleProps) {
</p>
);
}
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
type ProgressListProps = {
progress: UserProgressResponse;
customRoadmaps: UserProgressResponse;
teamRoadmaps?: HeroTeamRoadmaps;
isLoading?: boolean;
};
export function HeroRoadmaps(props: ProgressListProps) {
const { progress, isLoading = false, customRoadmaps } = props;
const {
teamRoadmaps = {},
progress,
isLoading = false,
customRoadmaps,
} = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState<string>();
return (
<div className="relative pb-12 pt-4 sm:pt-7">
<p className="mb-7 mt-2 text-sm">
<TeamAnnouncement />
</p>
{isCreatingRoadmap && (
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
<CreateRoadmapModal
teamId={creatingRoadmapTeamId}
onClose={() => {
setIsCreatingRoadmap(false);
setCreatingRoadmapTeamId(undefined);
}}
/>
)}
{
<HeroTitle
@@ -164,6 +182,83 @@ export function HeroRoadmaps(props: ProgressListProps) {
</div>
)}
</div>
{Object.keys(teamRoadmaps).map((teamName) => {
const currentTeam: UserProgressResponse[0]['team'] =
teamRoadmaps?.[teamName]?.[0]?.team;
const roadmapsList = teamRoadmaps[teamName].filter(
(roadmap) => !!roadmap.resourceTitle
);
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
return (
<div className="mt-5" key={teamName}>
{
<HeroTitle
icon={<Users2 className="mr-1.5 h-[14px] w-[14px]" />}
title={
<>
Team{' '}
<a
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
href={`/team/progress?t=${currentTeam?.id}`}
>
{teamName}
</a>
Roadmaps
</>
}
/>
}
{roadmapsList.length === 0 && (
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
Team does not have any roadmaps yet.{' '}
{canManageTeam && (
<button
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
onClick={() => {
setCreatingRoadmapTeamId(currentTeam?.id);
setIsCreatingRoadmap(true);
}}
>
Create one!
</button>
)}
</p>
)}
{roadmapsList.length > 0 && (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{roadmapsList.map((customRoadmap) => {
return (
<HeroRoadmap
key={customRoadmap.resourceId}
resourceId={customRoadmap.resourceId}
resourceType={'roadmap'}
resourceTitle={customRoadmap.resourceTitle}
percentageDone={
((customRoadmap.skipped + customRoadmap.done) /
customRoadmap.total) *
100
}
url={`/r?id=${customRoadmap.resourceId}`}
allowFavorite={false}
/>
);
})}
{canManageTeam && (
<CreateRoadmapButton
teamId={currentTeam?.id}
text="Create Team Roadmap"
/>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,12 +1,19 @@
---
import { FavoriteRoadmaps } from './FavoriteRoadmaps';
import {TeamAnnouncement} from "../TeamAnnouncement";
---
<div class='relative min-h-auto min-h-[192px] sm:min-h-[281px] border-b border-b-[#1e293c]'>
<div
class='min-h-auto relative min-h-[192px] border-b border-b-[#1e293c] sm:min-h-[281px] transition-all'
>
<div
class='container px-6 py-6 pb-14 text-left sm:px-0 sm:py-20 sm:text-center transition-opacity duration-300'
class='container px-5 py-6 pb-14 text-left transition-opacity duration-300 sm:px-0 sm:py-20 sm:text-center'
id='hero-text'
>
<p class='-mt-4 sm:-mt-10 mb-7'>
<TeamAnnouncement />
</p>
<h1
class='mb-2 bg-gradient-to-b from-amber-50 to-purple-500 bg-clip-text text-2xl font-bold text-transparent sm:mb-4 sm:text-5xl'
>
@@ -24,5 +31,5 @@ import { FavoriteRoadmaps } from './FavoriteRoadmaps';
their career.
</p>
</div>
<FavoriteRoadmaps client:only="react" />
<FavoriteRoadmaps client:only='react' />
</div>

View File

@@ -4,12 +4,14 @@ import { isLoggedIn } from '../../lib/jwt';
import { AccountDropdownList } from './AccountDropdownList';
import { DropdownTeamList } from './DropdownTeamList';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
export function AccountDropdown() {
const dropdownRef = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
useOutsideClick(dropdownRef, () => {
setShowDropdown(false);
@@ -22,6 +24,14 @@ export function AccountDropdown() {
return (
<div className="relative z-50 animate-fade-in">
{isCreatingRoadmap && (
<CreateRoadmapModal
onClose={() => {
setIsCreatingRoadmap(false);
}}
/>
)}
<button
className="flex h-8 w-40 items-center justify-center gap-1.5 rounded-full bg-gradient-to-r from-purple-500 to-purple-700 px-4 py-2 text-sm font-medium text-white hover:from-purple-500 hover:to-purple-600"
onClick={() => {
@@ -43,7 +53,13 @@ export function AccountDropdown() {
{isTeamsOpen ? (
<DropdownTeamList setIsTeamsOpen={setIsTeamsOpen} />
) : (
<AccountDropdownList setIsTeamsOpen={setIsTeamsOpen} />
<AccountDropdownList
onCreateRoadmap={() => {
setIsCreatingRoadmap(true);
setShowDropdown(false);
}}
setIsTeamsOpen={setIsTeamsOpen}
/>
)}
</div>
)}

View File

@@ -1,46 +1,76 @@
import { ChevronRight } from 'lucide-react';
import { ChevronRight, LogOut, Map, Plus, User2, Users2 } from 'lucide-react';
import { logout } from './navigation';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useState } from 'react';
type AccountDropdownListProps = {
onCreateRoadmap: () => void;
setIsTeamsOpen: (isOpen: boolean) => void;
};
export function AccountDropdownList(props: AccountDropdownListProps) {
const { setIsTeamsOpen } = props;
const { setIsTeamsOpen, onCreateRoadmap } = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
return (
<ul>
<li className="px-1">
<a
href="/account"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<User2 className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
Profile
</a>
</li>
<li className="px-1">
<a
href="/account/friends"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Friends
</a>
</li>
<li className="px-1">
<li className="mt-1 border-t border-t-gray-700/60 px-1 pt-1">
<button
className="group flex w-full items-center justify-between rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
onClick={() => {
onCreateRoadmap();
}}
className="group flex w-full items-center gap-2 rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Plus className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
New Roadmap
</button>
</li>
<li className="border-b border-b-gray-700/60 px-1 pb-1">
<a
href="/account/roadmaps"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Map className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Roadmaps
</a>
</li>
<li className="px-1 pt-1">
<button
className="group flex w-full items-center justify-between rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
onClick={() => setIsTeamsOpen(true)}
>
Teams
<span className="flex items-center gap-2.5">
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Teams
</span>
<ChevronRight className="h-4 w-4 shrink-0 stroke-[2.5px] text-slate-400 group-hover:text-white" />
</button>
</li>
<li className="px-1">
<button
className="block w-full rounded pl-4 pr-2 py-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex gap-2 items-center w-full rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
type="button"
onClick={logout}
>
<LogOut className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Logout
</button>
</li>

View File

@@ -2,7 +2,6 @@
import Icon from '../AstroIcon.astro';
import { AccountDropdown } from './AccountDropdown';
---
<div class='bg-slate-900 py-5 text-white sm:py-8'>
<nav class='container flex items-center justify-between'>
<a
@@ -27,8 +26,25 @@ import { AccountDropdown } from './AccountDropdown';
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a
>
</li>
<li class='hidden lg:inline'>
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
<li>
<a href='/teams' class='group relative text-blue-300 hover:text-white'>
Teams
<span
class='ml-0.5 hidden rounded-sm border-black bg-blue-300 px-1 py-0.5 text-xs font-semibold uppercase text-black group-hover:bg-white md:inline'
>
New
</span>
<span class='inline md:hidden absolute -right-[11px] top-0'>
<span class='relative flex h-2 w-2'>
<span
class='absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75'
></span>
<span class='relative inline-flex h-2 w-2 rounded-full bg-sky-500'
></span>
</span>
</span>
</a>
</li>
<li>
<kbd
@@ -45,7 +61,7 @@ import { AccountDropdown } from './AccountDropdown';
<a href='/login' class='text-gray-400 hover:text-white'>Login</a>
</li>
<li>
<AccountDropdown client:only="react" />
<AccountDropdown client:only='react' />
<a
data-guest-required

View File

@@ -105,7 +105,7 @@ export function PageSponsor(props: PageSponsorProps) {
</span>
<img
src={imageUrl}
className="block h-[150px] w-[104.89px] object-contain lg:h-[169px] lg:w-[118.18px]"
className="block h-[150px] object-fill lg:h-[169px] lg:w-[118.18px]"
alt="Sponsor Banner"
/>
<span className="flex flex-1 flex-col justify-between text-sm">

View File

@@ -1,66 +0,0 @@
import { CheckCircle, Copy } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
type CopyRoadmapLinkProps = {
roadmapId: string;
onClose: () => void;
};
export function CopyRoadmapLink(props: CopyRoadmapLinkProps) {
const { roadmapId, onClose } = props;
const baseUrl = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://roadmap.sh';
const shareLink = `${baseUrl}/r?id=${roadmapId}`;
const { copyText, isCopied } = useCopyText();
return (
<div className="flex grow flex-col justify-center">
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
<CheckCircle className="h-14 w-14 text-green-500" />
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
</div>
<input
type="text"
className="mt-6 w-full rounded-md border bg-gray-50 p-2 px-2.5 text-gray-700 focus:outline-none"
value={shareLink}
readOnly
onClick={(e) => {
e.currentTarget.select();
copyText(shareLink);
}}
/>
<p className="mt-1 text-sm text-gray-400">
You can share the above link with anyone who has access
</p>
<div className="mt-4 flex flex-col items-center justify-end gap-2">
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded bg-black px-4 py-2.5 text-sm font-medium text-white hover:opacity-80',
isCopied && 'bg-green-300 text-green-800'
)}
disabled={isCopied}
onClick={() => {
copyText(shareLink);
}}
>
<Copy className="h-3.5 w-3.5 stroke-[2.5]" />
{isCopied ? 'Copied' : 'Copy'}
</button>
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100'
)}
onClick={onClose}
>
Close
</button>
</div>
</div>
);
}

View File

@@ -1,4 +1,10 @@
import { type ReactNode, useCallback, useState } from 'react';
import {
type ReactNode,
useCallback,
useState,
useMemo,
useEffect,
} from 'react';
import { Globe2, Loader2, Lock } from 'lucide-react';
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
import { TransferToTeamList } from './TransferToTeamList';
@@ -7,7 +13,7 @@ import {
ShareTeamMemberList,
type TeamMemberList,
} from './ShareTeamMemberList';
import { CopyRoadmapLink } from './CopyRoadmapLink';
import { ShareSuccess } from './ShareSuccess';
import { useToast } from '../../hooks/use-toast';
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { httpPatch } from '../../lib/http';
@@ -16,6 +22,7 @@ import { cn } from '../../lib/classname';
import type { UserTeamItem } from '../TeamDropdown/TeamDropdown';
export type OnShareSettingsUpdate = (options: {
isDiscoverable: boolean;
visibility: AllowedRoadmapVisibility;
sharedTeamMemberIds: string[];
sharedFriendIds: string[];
@@ -24,10 +31,12 @@ export type OnShareSettingsUpdate = (options: {
type ShareOptionsModalProps = {
onClose: () => void;
visibility: AllowedRoadmapVisibility;
isDiscoverable?: boolean;
sharedFriendIds?: string[];
sharedTeamMemberIds?: string[];
teamId?: string;
roadmapId?: string;
description?: string;
onShareSettingsUpdate: OnShareSettingsUpdate;
};
@@ -36,11 +45,13 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
const {
roadmapId,
onClose,
isDiscoverable: defaultIsDiscoverable = false,
visibility: defaultVisibility,
sharedTeamMemberIds: defaultSharedMemberIds = [],
sharedFriendIds: defaultSharedFriendIds = [],
teamId,
onShareSettingsUpdate,
description,
} = props;
const toast = useToast();
@@ -49,9 +60,13 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
const [isSettingsUpdated, setIsSettingsUpdated] = useState(false);
const [friends, setFriends] = useState<ListFriendsResponse>([]);
const [teams, setTeams] = useState<UserTeamItem[]>([]);
const [members, setMembers] = useState<TeamMemberList[]>([]);
// Using global team members loading state to avoid glitchy UI when switching between teams
const [isTeamMembersLoading, setIsTeamMembersLoading] = useState(false);
const membersCache = useMemo(() => new Map<string, TeamMemberList[]>(), []);
const [visibility, setVisibility] = useState(defaultVisibility);
const [isDiscoverable, setIsDiscoverable] = useState(defaultIsDiscoverable);
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
defaultSharedMemberIds
);
@@ -104,6 +119,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
visibility,
sharedFriendIds,
sharedTeamMemberIds,
isDiscoverable,
}
);
@@ -114,11 +130,16 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
setIsLoading(false);
setIsSettingsUpdated(true);
onShareSettingsUpdate({ sharedFriendIds, visibility, sharedTeamMemberIds });
onShareSettingsUpdate({
isDiscoverable,
sharedFriendIds,
visibility,
sharedTeamMemberIds,
});
};
const handleTransferToTeam = useCallback(
async (teamId: string) => {
async (teamId: string, sharedTeamMemberIds: string[]) => {
if (!roadmapId) {
return;
}
@@ -128,6 +149,8 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
`${import.meta.env.PUBLIC_API_URL}/v1-transfer-roadmap/${roadmapId}`,
{
teamId,
sharedTeamMemberIds,
isDiscoverable,
}
);
@@ -149,7 +172,12 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
wrapperClassName="max-w-lg"
bodyClassName="p-4 flex flex-col"
>
<CopyRoadmapLink roadmapId={roadmapId!} onClose={onClose} />
<ShareSuccess
visibility={visibility}
roadmapId={roadmapId!}
description={description}
onClose={onClose}
/>
</Modal>
);
}
@@ -163,7 +191,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
onClose();
}}
wrapperClassName="max-w-3xl"
bodyClassName="p-4 flex flex-col min-h-[400px]"
bodyClassName="p-4 flex flex-col min-h-[440px]"
>
<div className="mb-4">
<h3 className="mb-1 text-xl font-semibold">Update Sharing Settings</h3>
@@ -195,6 +223,8 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
setSharedFriendIds([]);
setSharedTeamMemberIds([]);
}
setIsDiscoverable(visibility === 'public');
}}
/>
@@ -225,14 +255,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
setSharedFriendIds={setSharedFriendIds}
/>
)}
{canTransferRoadmap && (
<TransferToTeamList
teams={teams}
setTeams={setTeams}
selectedTeamId={selectedTeamId}
setSelectedTeamId={setSelectedTeamId}
/>
)}
{/* For Team Roadmap */}
{visibility === 'team' && teamId && (
@@ -240,12 +262,57 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
teamId={teamId}
sharedTeamMemberIds={sharedTeamMemberIds}
setSharedTeamMemberIds={setSharedTeamMemberIds}
members={members}
setMembers={setMembers}
membersCache={membersCache}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
/>
)}
{canTransferRoadmap && (
<>
<TransferToTeamList
teams={teams}
setTeams={setTeams}
selectedTeamId={selectedTeamId}
setSelectedTeamId={setSelectedTeamId}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
onTeamChange={() => {
setSharedTeamMemberIds([]);
}}
/>
{selectedTeamId && (
<>
<hr className="-mx-4 my-4" />
<div className="mb-4">
<ShareTeamMemberList
title="Select who can access this roadmap. You can change this later."
teamId={selectedTeamId!}
sharedTeamMemberIds={sharedTeamMemberIds}
setSharedTeamMemberIds={setSharedTeamMemberIds}
membersCache={membersCache}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
/>
</div>
</>
)}
</>
)}
</div>
{visibility !== 'me' && (
<>
<hr className="-mx-4 my-4" />
<div className="mb-2">
<DiscoveryCheckbox
isDiscoverable={isDiscoverable}
setIsDiscoverable={setIsDiscoverable}
/>
</div>
</>
)}
<div className="mt-2 flex items-center justify-between gap-1.5">
<button
className="flex items-center justify-center gap-1.5 rounded-md border px-3.5 py-1.5 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-75"
@@ -255,21 +322,28 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
Close
</button>
{canTransferRoadmap ? (
{canTransferRoadmap && (
<UpdateAction
disabled={isUpdateDisabled || isLoading}
disabled={
isUpdateDisabled || isLoading || sharedTeamMemberIds.length === 0
}
onClick={() => {
handleTransferToTeam(selectedTeamId!).then(() => null);
handleTransferToTeam(selectedTeamId!, sharedTeamMemberIds).then(
() => null
);
}}
>
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
Transfer
</UpdateAction>
) : (
)}
{!canTransferRoadmap && (
<UpdateAction
disabled={isUpdateDisabled || isLoading}
onClick={() => {
handleShareChange({
isDiscoverable,
visibility,
sharedTeamMemberIds:
visibility === 'team' ? sharedTeamMemberIds : [],
@@ -309,3 +383,25 @@ function UpdateAction(props: {
</button>
);
}
type DiscoveryCheckboxProps = {
isDiscoverable: boolean;
setIsDiscoverable: (isDiscoverable: boolean) => void;
};
function DiscoveryCheckbox(props: DiscoveryCheckboxProps) {
const { isDiscoverable, setIsDiscoverable } = props;
return (
<label className="group flex items-center gap-1.5">
<input
type="checkbox"
checked={isDiscoverable}
onChange={(e) => setIsDiscoverable(e.target.checked)}
/>
<span className="text-sm text-gray-500 group-hover:text-gray-700">
Include on discovery page (when launched)
</span>
</label>
);
}

View File

@@ -0,0 +1,132 @@
import { CheckCircle, Copy, Facebook, Linkedin, Twitter } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
type ShareSuccessProps = {
roadmapId: string;
onClose: () => void;
visibility: AllowedRoadmapVisibility;
description?: string;
isSharingWithOthers?: boolean;
};
export function ShareSuccess(props: ShareSuccessProps) {
const {
roadmapId,
onClose,
description,
visibility,
isSharingWithOthers = false,
} = props;
const baseUrl = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://roadmap.sh';
const shareLink = `${baseUrl}/r?id=${roadmapId}`;
const { copyText, isCopied } = useCopyText();
const socialShareLinks = [
{
title: 'Twitter',
href: `https://twitter.com/intent/tweet?text=${description}&url=${shareLink}`,
icon: Twitter,
},
{
title: 'Facebook',
href: `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${shareLink}`,
icon: Facebook,
},
{
title: 'Linkedin',
href: `https://www.linkedin.com/sharing/share-offsite/?url=${shareLink}`,
icon: Linkedin,
},
];
return (
<div className="flex grow flex-col justify-center">
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
<CheckCircle className="h-14 w-14 text-green-500" />
{isSharingWithOthers ? (
<h3 className="text-xl font-medium">Sharing with Others</h3>
) : (
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
)}
</div>
<input
type="text"
className="mt-6 w-full rounded-md border bg-gray-50 p-2 px-2.5 text-gray-700 focus:outline-none"
value={shareLink}
readOnly
onClick={(e) => {
e.currentTarget.select();
copyText(shareLink);
}}
/>
{isSharingWithOthers ? (
<p className="mt-1 text-sm text-gray-400">
You can share the above link with anyone
</p>
) : (
<p className="mt-1 text-sm text-gray-400">
You can share the above link with anyone who has access
</p>
)}
{visibility === 'public' && (
<>
<div className="-mx-4 mt-4 flex items-center gap-1.5">
<span className="h-px grow bg-gray-300" />
<span className="px-2 text-xs uppercase text-gray-400">Or</span>
<span className="h-px grow bg-gray-300" />
</div>
<div className="mt-4 flex items-center gap-2">
<span className="text-sm text-gray-600">Share with others on</span>
<ul className="flex items-center gap-1.5">
{socialShareLinks.map((socialShareLink) => (
<li key={socialShareLink.title}>
<a
href={socialShareLink.href}
target="_blank"
rel="noopener noreferrer"
className="flex h-8 w-8 items-center justify-center gap-1.5 rounded-md border bg-gray-50 text-sm text-gray-700 hover:bg-gray-100 focus:outline-none"
>
<socialShareLink.icon className="h-4 w-4" />
</a>
</li>
))}
</ul>
</div>
</>
)}
<div className="mt-4 flex flex-col items-center justify-end gap-2">
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded bg-black px-4 py-2.5 text-sm font-medium text-white hover:opacity-80',
isCopied && 'bg-green-300 text-green-800'
)}
disabled={isCopied}
onClick={() => {
copyText(shareLink);
}}
>
<Copy className="h-3.5 w-3.5 stroke-[2.5]" />
{isCopied ? 'Copied' : 'Copy URL'}
</button>
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100'
)}
onClick={onClose}
>
Close
</button>
</div>
</div>
);
}

View File

@@ -33,27 +33,31 @@ export interface TeamMemberList extends TeamMemberDocument {
type ShareTeamMemberListProps = {
teamId: string;
setMembers: (members: TeamMemberList[]) => void;
members: TeamMemberList[];
title?: string;
sharedTeamMemberIds: string[];
setSharedTeamMemberIds: (sharedTeamMemberIds: string[]) => void;
membersCache: Map<string, TeamMemberList[]>;
isTeamMembersLoading: boolean;
setIsTeamMembersLoading: (isLoading: boolean) => void;
};
export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
const {
setMembers,
members,
teamId,
title = 'Select Members',
sharedTeamMemberIds,
setSharedTeamMemberIds,
teamId,
membersCache,
isTeamMembersLoading: isLoading,
setIsTeamMembersLoading: setIsLoading,
} = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
async function loadTeamMembers() {
if (members?.length > 0) {
if (membersCache.has(teamId)) {
return;
}
@@ -67,21 +71,23 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
return;
}
setMembers(response);
const joinedMembers =
response?.filter((member) => member.status === 'joined') || [];
membersCache.set(teamId, joinedMembers);
}
useEffect(() => {
loadTeamMembers().finally(() => {
setIsLoading(false);
});
}, []);
}, [teamId]);
const loadingMembers = isLoading && (
<ul className="mt-2 grid grid-cols-3 gap-2.5">
{[...Array(3)].map((_, idx) => (
<li
key={idx}
className="flex min-h-[62px] animate-pulse items-center gap-2 rounded-md border p-2"
className="flex min-h-[66px] animate-pulse items-center gap-2 rounded-md border p-2"
>
<div className="h-8 w-8 shrink-0 rounded-full bg-gray-200" />
<div className="inline-grid w-full">
@@ -93,11 +99,13 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
</ul>
);
const members = membersCache.get(teamId) || [];
return (
<>
{(members.length > 0 || isLoading) && (
<div className="flex items-center justify-between gap-2">
<p className="text-sm">Select Members</p>
<p className="text-sm">{title}</p>
<label className="flex items-center gap-2">
<input

View File

@@ -11,10 +11,22 @@ type TransferToTeamListProps = {
selectedTeamId: string | null;
setSelectedTeamId: (teamId: string | null) => void;
isTeamMembersLoading: boolean;
setIsTeamMembersLoading: (isLoading: boolean) => void;
onTeamChange: (teamId: string | null) => void;
};
export function TransferToTeamList(props: TransferToTeamListProps) {
const { teams, setTeams, selectedTeamId, setSelectedTeamId } = props;
const {
teams,
setTeams,
selectedTeamId,
setSelectedTeamId,
isTeamMembersLoading,
setIsTeamMembersLoading,
onTeamChange,
} = props;
const toast = useToast();
@@ -73,11 +85,17 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
<li key={team._id}>
<button
className={cn(
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5',
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5 disabled:cursor-not-allowed disabled:opacity-70',
isSelected && 'border-gray-500 bg-gray-100 text-black'
)}
disabled={isTeamMembersLoading}
onClick={() => {
setSelectedTeamId(team._id);
if (isSelected) {
setSelectedTeamId(null);
} else {
setSelectedTeamId(team._id);
}
onTeamChange(team._id);
}}
>
<img

View File

@@ -0,0 +1,16 @@
type TeamAnnouncementProps = {};
export function TeamAnnouncement(props: TeamAnnouncementProps) {
return (
<a
className="rounded-md border border-dashed border-purple-700 px-3 py-1.5 text-purple-400 transition-colors hover:border-gray-700 hover:text-white"
href="/teams"
>
<span className="relative -top-[0.5px] mr-1 text-xs font-semibold uppercase text-white">
New
</span>{' '}
<span className={'hidden sm:inline'}>Announcing roadmaps for teams. <span className='font-semibold'>Learn more!</span></span>
<span className={'inline sm:hidden'}>Announcing roadmaps for teams!</span>
</a>
);
}

View File

@@ -0,0 +1,164 @@
import { useEffect, useState } from 'react';
import { cn } from '../../lib/classname.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import { fireTeamCreationClick } from './TeamHeroBanner.tsx';
const demoItems = [
{
title: 'Roadmap Editor',
description:
'<span class="font-semibold">Powerful editor</span> to create custom roadmaps and other trackable documents',
image: '/images/team-promo/roadmap-editor.png',
},
{
title: 'Invite Members',
description:
'Invite your <span class="font-semibold">team members and assign roles</span>',
image: '/images/team-promo/invite-members.png',
},
{
title: 'Track Progress',
description:
'You and your team can <span class="font-semibold">track progress</span> on the roadmaps',
image: '/images/team-promo/update-progress.png',
},
{
title: 'Team Dashboard',
description:
'Keep an eye on the team progress through <span class="font-semibold">team dashboards</span>',
image: '/images/team-promo/team-dashboard.png',
},
{
title: 'Roadmaps and Documents',
description:
'Create as many <span class="font-semibold">roadmaps or trackable documents</span> as you want',
image: '/images/team-promo/many-roadmaps.png',
},
{
title: 'Community Roadmaps',
description:
'Create custom roadmaps or customize <span class="font-semibold">community roadmaps</span> to fit your needs',
image: '/images/team-promo/our-roadmaps.png',
},
{
title: 'Sharing Settings',
description:
'Share a roadmap or trackable document with <span class="font-semibold">everyone or specific people</span>',
image: '/images/team-promo/sharing-settings.png',
},
{
title: 'More Coming Soon!',
description:
'<span class="font-semibold">We have a lot more coming soon!</span>',
},
];
export function TeamDemo() {
const [hasViewed, setHasViewed] = useState<number[]>([0]);
const [activeItem, setActiveItem] = useState(demoItems[0]);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="hidden border-t py-12 sm:block">
<div className="container">
<h2 className="mb-2 text-3xl font-bold">See how it works</h2>
<p>Here is a sneak peek of what you can do today (more coming soon!)</p>
<div className="relative mt-7 flex flex-row items-center gap-2.5">
{demoItems.map((item, counter) => {
const isActive = item === activeItem;
const hasAlreadyViewed = hasViewed.includes(counter);
if (!isActive) {
return (
<span key={item.title} className="relative flex items-center">
<span
onClick={() => {
setHasViewed([...hasViewed, counter]);
setActiveItem(item);
}}
className={cn('z-50 cursor-pointer rounded-full p-[6px]', {
'bg-black': item === activeItem,
'bg-gray-300 hover:bg-gray-400': item !== activeItem,
})}
/>
{!hasAlreadyViewed && (
<span className="pointer-events-none absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-400 opacity-75"></span>
)}
</span>
);
}
return (
<span
key={item.title}
className=" rounded-full bg-black px-3 py-0.5 text-sm text-white"
>
{activeItem.title}
</span>
);
})}
</div>
<div className="mt-4 overflow-hidden rounded-xl border border-gray-300">
<div className="p-3">
<p
className="text-base text-black"
dangerouslySetInnerHTML={{ __html: activeItem.description }}
/>
</div>
{activeItem.image && (
<img
className="rounded-b-xl border-t"
src={activeItem.image}
alt=""
/>
)}
{!activeItem.image && (
<div className="bg-gray-50 py-4 pl-3">
<p className="mb-3">
Register your team now and help us shape the future of teams in
roadmap.sh!
</p>
<div className="flex flex-row items-center gap-2">
<a
onClick={() => {
fireTeamCreationClick();
if (isAuthenticated) {
return;
}
localStorage.setItem('authRedirect', '/team/new');
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-purple-600 px-5 py-2 text-base font-medium text-white hover:bg-purple-700"
>
Create your Team
</a>
{!isAuthenticated && (
<span className="ml-1 text-base">
or &nbsp;
<a
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
href="/login"
className="text-purple-600 underline hover:text-purple-700"
>
Login to your account
</a>
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { CheckCircle, CheckCircle2, CheckIcon } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt.ts';
import { useEffect, useState } from 'react';
const featureList = [
'Create custom roadmaps for your team',
"Plan, track and document your team's skills and growth",
'Invite your team members',
"Get insights on your team's skills and growth",
];
export function fireTeamCreationClick() {
window.fireEvent({
category: 'FeatureClick',
action: `Pages / Teams`,
label: 'Create your Team',
});
}
export function TeamHeroBanner() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="bg-white py-8 lg:py-12">
<div className="container">
<div className="flex flex-row items-center justify-start text-left lg:justify-between">
<div className="flex flex-grow flex-col">
<h1 className="mb-0.5 text-2xl font-bold sm:mb-2.5 sm:text-4xl lg:mb-4 lg:text-5xl">
Roadmaps for Teams
</h1>
<p className="mb-4 text-base leading-normal text-gray-600 sm:mb-0 sm:leading-none lg:text-lg">
Train, plan and track your team's skills and career growth.
</p>
<ul className="mb-4 mt-0 hidden text-sm leading-7 sm:mb-4 sm:mt-4 sm:flex sm:flex-col lg:mb-6 lg:mt-6 lg:text-base lg:leading-8">
{featureList.map((feature, index) => (
<li key={feature}>
<CheckCircle className="hidden h-6 w-6 text-green-500 sm:inline-block" />
<span className="ml-0 sm:ml-2">{feature}</span>
</li>
))}
</ul>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<a
onClick={() => {
fireTeamCreationClick();
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="flex w-full items-center justify-center rounded-lg border border-transparent bg-purple-600 px-5 py-2 text-sm font-medium text-white hover:bg-blue-700 sm:w-auto sm:text-base"
>
Create your Team
</a>
{!isAuthenticated && (
<>
<span className="ml-1 hidden text-base sm:inline">
or &nbsp;
<a
href="/login"
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
className="text-purple-600 underline hover:text-purple-700"
>
Login to your account
</a>
</span>
<a
href="/login"
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
className="flex w-full items-center justify-center rounded-lg border border-purple-600 px-5 py-2 text-base text-sm font-medium text-purple-600 hover:bg-blue-700 sm:hidden sm:text-base"
>
Login to your account
</a>
</>
)}
</div>
</div>
<img
alt={'team roadmaps'}
className="hidden h-64 md:block lg:h-80"
src="/images/team-promo/hero-img.png"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { Check, CheckCircle, Copy, Sparkles } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text.ts';
import { cn } from '../../lib/classname.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import { fireTeamCreationClick } from './TeamHeroBanner.tsx';
import { useEffect, useState } from 'react';
export function TeamPricing() {
const { isCopied, copyText } = useCopyText();
const teamEmail = 'teams@roadmap.sh';
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="border-t py-4 sm:py-8 md:py-12">
<div className="container">
<h2 className="mb-1 text-xl font-bold sm:mb-1.5 sm:text-2xl md:mb-2 md:text-3xl">
Beta Pricing
</h2>
<p className="mb-4 text-base text-gray-600 sm:mb-8 sm:text-lg">
We are currently in public beta and are offering free access to all
features.
</p>
<div className="flex flex-col gap-6 sm:flex-row sm:gap-4">
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-purple-500">
<div className="px-8 pb-2 pt-5 text-center sm:pt-4">
<h3 className="mb-1 text-2xl font-bold">Free</h3>
<p className="text-sm text-gray-500">No credit card required</p>
<p className="flex items-start justify-center gap-1 py-6 text-3xl">
<span className="text-base text-gray-600">$</span>
<span className="text-5xl font-bold">0</span>
</p>
<a
onClick={() => {
fireTeamCreationClick();
if (isAuthenticated) {
return;
}
localStorage.setItem('redirect', '/team/new');
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="block rounded-md bg-purple-600 px-6 py-2 text-center text-sm font-medium leading-6 text-white shadow transition hover:bg-gray-700 hover:shadow-lg focus:outline-none"
>
{isAuthenticated ? 'Create your Team' : 'Sign up for free'}
</a>
</div>
<div className="flex w-full flex-col gap-1 border-t px-8 py-5 text-center sm:py-3">
<p className="font-semibold text-black">All the features</p>
<p className="text-gray-600">Roles and Permissions</p>
<p className="text-gray-600">Custom Roadmaps</p>
<p className="text-gray-600">Sharing Options</p>
<p className="text-gray-600">Progress Tracking</p>
<p className="text-gray-600">Team Insights</p>
<p className="text-gray-600">Onboarding support</p>
</div>
</div>
<div className="flex flex-grow flex-col items-center justify-center rounded-md border border-gray-300 py-8">
<img
alt={'waving hand'}
src={'/images/team-promo/contact.png'}
className="mb-3 h-40"
/>
<p className="mb-2 font-medium text-gray-500">
Questions? We are here to help!
</p>
<p className="text-gray-600">
<button
onClick={() => {
copyText(teamEmail);
}}
className={cn(
'relative flex items-center justify-between gap-3 overflow-hidden rounded-md border border-black bg-white px-4 py-2 text-black hover:bg-gray-100'
)}
>
{teamEmail}
<Copy
className="relative top-[1px] ml-2 inline-block text-black transition-opacity"
size={16}
/>
<span
className={cn(
'absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black text-white transition-all',
{
'top-full': !isCopied,
'top-0': isCopied,
}
)}
>
Email copied!
</span>
</button>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
const toolsList = [
{
imageUrl: '/images/team-promo/growth-plans.png',
title: 'Growth plans',
description: 'Prepare shared or individual growth plans for members.',
},
{
imageUrl: '/images/team-promo/progress-tracking.png',
title: 'Progress tracking',
description: 'Track and compare the progress of team members.',
},
{
imageUrl: '/images/team-promo/onboarding.png',
title: 'Onboarding',
description: 'Prepare onboarding plans for new team members.',
},
{
imageUrl: '/images/team-promo/team-insights.png',
title: 'Team insights',
description: 'Get insights about your team skills, progress and more.',
},
{
imageUrl: '/images/team-promo/skill-gap.png',
title: 'Skill gap analysis',
description: 'Understand the skills of your team and identify gaps.',
},
{
imageUrl: '/images/team-promo/documentation.png',
title: 'Documentation',
description: 'Create and share visual team documentation.',
},
];
export function TeamTools() {
return (
<div className="py-4 sm:py-8 md:py-12 border-t">
<div className="container">
<h2 className="mb-1 sm:mb-1.5 md:mb-2 text-xl sm:text-2xl md:text-3xl font-bold">Track and guide your teams knowledge</h2>
<p className='text-sm md:text-base'>
Individual and team level growth plans, progress tracking, skill gap analysis, team insights and more.
</p>
<div className="mt-3 sm:mt-5 md:mt-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 sm:gap-4">
{toolsList.map((tool) => {
return (
<div className="rounded-md sm:rounded-xl border p-2 sm:p-5 text-left sm:text-center md:text-left">
<img
alt={tool.title}
src={tool.imageUrl}
className="mb-5 h-48 hidden sm:block mx-auto md:mx-0"
/>
<h3 className="mb-0.5 sm:mb-2 text-lg sm:text-2xl font-bold">{tool.title}</h3>
<p className='text-sm sm:text-base'>{tool.description}</p>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -27,6 +27,7 @@ import {
import { RoadmapActionDropdown } from './RoadmapActionDropdown';
import { UpdateTeamResourceModal } from '../CreateTeam/UpdateTeamResourceModal';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
import { cn } from '../../lib/classname';
export function TeamRoadmaps() {
const { t: teamId } = getUrlParams();
@@ -311,6 +312,7 @@ export function TeamRoadmaps() {
const shareSettingsModal = selectedResource && (
<ShareOptionsModal
description={selectedResource.description!}
visibility={selectedResource.visibility!}
sharedTeamMemberIds={selectedResource.sharedTeamMemberIds!}
sharedFriendIds={selectedResource.sharedFriendIds!}
@@ -427,7 +429,12 @@ export function TeamRoadmaps() {
return (
<div
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
className={cn(
'grid grid-cols-1 p-2.5',
canManageCurrentTeam
? 'sm:grid-cols-[auto_172px]'
: 'sm:grid-cols-[auto_110px]'
)}
key={resourceConfig.resourceId}
>
<div className="mb-3 grid grid-cols-1 sm:mb-0">
@@ -478,6 +485,18 @@ export function TeamRoadmaps() {
<ExternalLink className="inline-block h-4 w-4" />
Visit
</a>
{canManageCurrentTeam && (
<a
href={editorLink}
className={
'ml-2 flex items-center gap-2 rounded-md border border-gray-800 bg-gray-900 px-2.5 py-1.5 text-xs text-white hover:bg-gray-800 focus:outline-none'
}
target={'_blank'}
>
<PenSquare className="inline-block h-4 w-4" />
Edit
</a>
)}
</div>
</div>
);

View File

@@ -0,0 +1,27 @@
Let's see how we can use the `alert`, `prompt` and `confirm` functions to interact with the user.
## alert()
The `alert()` method displays an alert box with a specified message and an OK button.
```js
alert('Hello World!');
```
## prompt()
The `prompt()` method displays a dialog box that prompts the visitor for input. A prompt box is often used if you want the user to input a value before entering a page. The `prompt()` method returns the input value if the user clicks OK. If the user clicks Cancel, the method returns `null`.
```js
const name = prompt('What is your name?');
console.log(name);
```
## confirm()
The `confirm()` method displays a dialog box with a specified message, along with an OK and a Cancel button. This is often used to confirm or verify something from the user.
```js
const result = confirm('Are you sure?');
console.log(result); // true/false
```

View File

@@ -0,0 +1,32 @@
You can add a new element to the DOM using the `appendChild` or `insertBefore` method.
## appendChild
The `appendChild` method adds a new element as the last child of the specified parent element.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmap = document.createElement('div');
roadmap.id = 'javascript-roadmap';
roadmapWrapper.appendChild(roadmapTitle);
```
In the example above, the `roadmap` element is added as the last child of the `roadmapWrapper` element.
## insertBefore
The `insertBefore` method adds a new element before the specified child element.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmap = document.createElement('div');
roadmap.id = 'javascript-roadmap';
const roadmapTitle = document.querySelector('#roadmap-title');
roadmapWrapper.insertBefore(roadmap, roadmapTitle);
```
In the example above, the `roadmap` element is added before the `roadmapTitle` element.

View File

@@ -0,0 +1,27 @@
The difference between Asynchronous and Synchronous code is that Asynchronous code does not block the execution of the program while Synchronous code does.
## Asynchronous code
Asynchronous code is executed in the background and it does not block the execution of the program. It is usually used to perform tasks that take a long time to complete, such as network requests.
```js
console.log('Before');
setTimeout(() => {
console.log('Hello');
}, 1000);
console.log('After');
```
## Synchronous code
Synchronous code is executed in sequence and it blocks the execution of the program until it is completed. If a task takes a long time to complete, everything else waits.
```js
console.log('Before');
for (let i = 0; i < 1000000000; i++) {}
console.log('After');
```

View File

@@ -0,0 +1,28 @@
You can use `break` and `continue` in loops to alter the flow of the loop. `break` will stop the loop from continuing, and `continue` will skip the current iteration and continue the loop.
```js
for (let i = 0; i < 5; i++) {
if (i === 1) {
continue; // skips the rest of the code in the loop
}
console.log(`i: ${i}`);
}
// Output:
// i: 0
// i: 2
// i: 3
// i: 4
```
```js
for (let i = 0; i < 5; i++) {
if (i === 1) {
break; // stops the loop
}
console.log(`i: ${i}`);
}
// Output:
// i: 0
```

View File

@@ -0,0 +1,48 @@
**Callback hell**, often referred to as **Pyramid of Doom**, describes a situation in JavaScript where multiple nested callbacks become difficult to manage, leading to unreadable and unmaintainable code. It often arises when performing multiple asynchronous operations that depend on the completion of previous operations. The code starts to take on a pyramidal shape due to the nesting.
## Example of callback hell
```js
callAsync1(function () {
callAsync2(function () {
callAsync3(function () {
callAsync4(function () {
callAsync5(function () {
// ...
});
});
});
});
});
```
## Strategies to avoid callback hell
Developers can address or avoid callback hell by using strategies like modularizing the code into named functions, using asynchronous control flow libraries, or leveraging modern JavaScript features like Promises and `async/await` to write more linear, readable asynchronous code.
### Promise chaining
```js
callAsync1()
.then(() => callAsync2())
.then(() => callAsync3())
.then(() => callAsync4())
.then(() => callAsync5())
.catch((err) => console.error(err));
```
### Async/await
```js
async function asyncCall() {
try {
await callAsync1();
await callAsync2();
await callAsync3();
await callAsync4();
await callAsync5();
} catch (err) {
console.error(err);
}
}
```

View File

@@ -0,0 +1,18 @@
A closure is a function that has access to its outer function scope even after the outer function has returned. This means a closure can remember and access variables and arguments of its outer function even after the function has finished.
```js
function outer() {
const name = 'Roadmap';
function inner() {
console.log(name);
}
return inner;
}
const closure = outer();
closure(); // Roadmap
```
In the above example, the `inner` function has access to the `name` variable of the `outer` function even after the `outer` function has returned. Therefore, the `inner` function forms a closure.

View File

@@ -0,0 +1,8 @@
The Comma Operator `,` evaluates each of its operands (from left to right) and returns the value of the last operand.
```js
let x = 1;
x = (x++, x);
console.log(x); // 2
```

View File

@@ -0,0 +1,9 @@
To create a new DOM element, you can use the `document.createElement` method. It accepts a tag name as an argument and returns a new element with the specified tag name. You can set attributes to the element.
```js
const div = document.createElement('div');
div.id = 'roadmap-wrapper';
div.setAttribute('data-id', 'javascript');
console.log(div); // <div id="roadmap-wrapper" data-id="javascript"></div>
```

View File

@@ -0,0 +1,33 @@
You can use the `CustomEvent` constructor to create a custom event. The `CustomEvent` constructor accepts two arguments: the event name and an optional object that specifies the event options. And you can use the `dispatchEvent` method to dispatch the custom event on the target element/document.
## Creating Custom Events
```js
const event = new CustomEvent('roadmap-updated', {
detail: { name: 'JavaScript' },
});
element.dispatchEvent(event);
```
## Listening for Custom Events
You can listen for custom events using the `addEventListener` method. The `addEventListener` method accepts the event name and a callback function that is called when the event is dispatched.
```js
element.addEventListener('roadmap-updated', (event) => {
console.log(event.detail); // { name: 'JavaScript' }
});
```
## Removing Event Listeners
You can remove event listeners using the `removeEventListener` method. The `removeEventListener` method accepts the event name and the callback function that was used to add the event listener.
```js
function handleEvent(event) {
console.log(event.detail); // { name: 'JavaScript' }
}
element.addEventListener('roadmap-updated', handleEvent);
element.removeEventListener('roadmap-updated', handleEvent);
```

View File

@@ -0,0 +1,38 @@
Debugging JavaScript code can be achieved through various methods and tools. Here's a basic guide:
## Console Logging:
You can use `console.log()`, `console.warn()`, `console.error()`, etc., to print values, variables, or messages to the browser's developer console.
```js
console.log('Value of x:', x);
```
## Browser Developer Tools:
Most modern browsers come equipped with developer tools. You can access these tools by pressing `F12` or right-clicking on the web page and selecting `Inspect` or `Inspect Element`.
- **Sources Tab**: Allows you to see the loaded scripts, set breakpoints, and step through the code.
- **Console Tab**: Displays console outputs and allows for interactive JavaScript execution.
- **Network Tab**: Helps in checking network requests and responses.
## Setting Breakpoints:
In the `Sources` tab of the browser's developer tools, you can click on a line number to set a breakpoint. The code execution will pause at this line, allowing you to inspect variables, the call stack, and continue step-by-step.
## Debugger Statement:
Inserting the `debugger;` statement in your code will act as a breakpoint when the browser developer tools are open. Execution will pause at the `debugger;` line.
```js
function myFunction() {
debugger; // Execution will pause here when dev tools are open
// ... rest of the code
}
```
## Call Stack and Scope:
In the developer tools, when paused on a breakpoint or `debugger;` statement, you can inspect the `call stack` to see the sequence of function calls. The `Scope` panel will show you the values of local and global variables.
Remember, debugging is an iterative process. It often involves setting breakpoints, checking variables, adjusting code, and re-running to ensure correctness.

View File

@@ -0,0 +1,25 @@
The main difference between `defer` and `async` is the order of execution.
## Defer attribute
A `<script>` element with a `defer` attribute, it will continue to load the HTML page and render it while the script is being downloaded. The script is executed after the HTML page has been completely parsed. `defer` scripts maintain their order in the document.
```html
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
```
In the example above, `script1.js` will be executed before `script2.js`. The browser will download both scripts in parallel, but `script1.js` will be executed after the HTML page has been parsed and `script2.js` will be executed after `script1.js` has been executed.
## Async attribute
On the other hand, A `<script>` element with an `async` attribute, it will pause the HTML parser and execute the script immediately after it has been downloaded. The HTML parsing will resume after the script has been executed.
```html
<script async src="script1.js"></script>
<script async src="script2.js"></script>
```
In the example above, the browser will download both scripts in parallel, and execute them as soon as they are downloaded. The order of execution is not guaranteed.
To know more you can check [this diagram](https://roadmap.sh/guides/avoid-render-blocking-javascript-with-async-defer) from us that explains the difference between `defer` and `async` in a visual way.

View File

@@ -0,0 +1,14 @@
The `do...while` statement creates a loop that executes a block of code once, before checking if the condition is `true`, then it will repeat the loop as long as the condition is `true`.
```js
let i = 0;
do {
console.log(i);
i++;
} while (i < 3);
// 0
// 1
// 2
```

View File

@@ -0,0 +1,7 @@
The `==` equality operator converts the operands if they are not of the same type, then applies strict comparison. The `===` strict equality operator only considers values equal that have the same type.
```js
console.log(1 == '1'); // true
console.log(1 === '1'); // false
console.log(1 === 1); // true
```

View File

@@ -0,0 +1,24 @@
In order to handle errors in async/await, we can use the `try/catch` statement.
## Rejecting a promise
```js
const promise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
```
## Try/catch statement
```js
async function main() {
try {
const result = await promise;
console.log(result);
} catch (error) {
console.log(error.message);
}
}
```
The `catch` block will be executed when the promise is `rejected` or when an error is thrown inside the `try` block.

View File

@@ -0,0 +1,38 @@
In order to handle errors in promises, we can use the `catch` method or the second argument of the `then` method.
## Rejecting a promise
```js
const promise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
```
## Catch method
In this method, we can pass a `callback` function that will be called when the promise is `rejected`.
```js
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error.message);
});
```
## Second argument of the then method
In this method, we can pass two `callback` functions as arguments. The first one will be called when the promise is `resolved` and the second one will be called when the promise is `rejected`.
```js
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error.message);
}
);
```

View File

@@ -0,0 +1,19 @@
Event bubbling is a concept in the Document Object Model (DOM) that describes the way in which events propagate or "bubble up" through the hierarchy of nested elements in the DOM.
When an event, such as a mouse click, occurs on a DOM element, the event will be handled by the element first, then its parent element, and so on, until the event reaches the root element. This behavior is called event bubbling.
```js
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Scenario of clicking on the child element
parent.addEventListener('click', () => {
console.log('Handled Last');
});
child.addEventListener('click', () => {
console.log('Handled First');
});
```
In the above example, when you click on the `child` element, the event will be handled by the `child` element first, then its parent element, and so on, to the root element unless you stop the propagation (`event.stopPropagation()`) of the event.

View File

@@ -0,0 +1,26 @@
The Event loop has two main components: the Call stack and the Callback queue.
## Call Stack
The Call stack is a data structure that stores the tasks that need to be executed. It is a LIFO (Last In, First Out) data structure, which means that the last task that was added to the Call stack will be the first one to be executed.
## Callback Queue
The Callback queue is a data structure that stores the tasks that have been completed and are ready to be executed. It is a FIFO (First In, First Out) data structure, which means that the first task that was added to the Callback queue will be the first one to be executed.
## Event Loop's Workflow:
1. Executes tasks from the Call Stack.
2. For an asynchronous task, such as a timer, it runs in the background. JavaScript proceeds to the next task without waiting.
3. When the asynchronous task concludes, its callback function is added to the Callback Queue.
4. If the Call Stack is empty and there are tasks in the Callback Queue, the Event Loop transfers the first task from the Queue to the Call Stack for execution.
```js
setTimeout(() => console.log('Hello from the timer'), 0);
console.log('Hello from the main code');
```
1. `setTimeout` is processed, and because it's asynchronous, its callback is placed in the Callback Queue.
2. The next line, `console.log("Hello from the main code")`, is logged immediately.
3. Although the timer duration is 0 milliseconds, its callback has to wait until the Call Stack is empty. After the main code logs, the callback is moved from the Callback Queue to the Call Stack and executed.
4. The result is "Hello from the main code" being logged before "Hello from the timer".

View File

@@ -0,0 +1,19 @@
Explicit binding is a way to explicitly state what the `this` keyword is going to be bound to using `call`, `apply` or `bind` methods of a function.
```js
const roadmap = {
name: 'JavaScript',
};
function printName() {
console.log(this.name);
}
printName.call(roadmap); // JavaScript
printName.apply(roadmap); // JavaScript
const printRoadmapName = printName.bind(roadmap);
printRoadmapName(); // JavaScript
```
In the above example, the `this` keyword inside the `printName()` function is explicitly bound to the `roadmap` object using `call`, `apply` or `bind` methods.

View File

@@ -0,0 +1,12 @@
You can use the `filter()` method to filter an array based on a condition. The `filter()` method creates a new array with all elements that pass the test implemented by the provided function.
```js
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter((number) => {
return number % 2 === 0;
});
console.log(numbers); // [1, 2, 3, 4, 5, 6]
console.log(evenNumbers); // [2, 4, 6]
```

View File

@@ -0,0 +1,14 @@
The `finally` block will be executed when the promise is `resolved` or `rejected`.
```js
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.log(error.message);
})
.finally(() => {
console.log('Finally Promise has settled');
});
```

View File

@@ -0,0 +1,55 @@
There are serveral ways to find unique values in an array. Here are some of them:
## Using `Set`
```js
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
const uniqueRoadmaps = [...new Set(roadmaps)];
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
```
## Using `filter()`
```js
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
const uniqueRoadmaps = roadmaps.filter(
(roadmap, index) => roadmaps.indexOf(roadmap) === index
);
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
```
## Using `reduce()`
```js
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
const uniqueRoadmaps = roadmaps.reduce((unique, roadmap) => {
return unique.includes(roadmap) ? unique : [...unique, roadmap];
}, []);
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
```
## Using `forEach()`
```js
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
const uniqueRoadmaps = [];
roadmaps.forEach((roadmap) => {
if (!uniqueRoadmaps.includes(roadmap)) {
uniqueRoadmaps.push(roadmap);
}
});
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
```
## Using `for...of`
```js
const roadmaps = ['JavaScript', 'React', 'Node.js', 'Node.js', 'JavaScript'];
const uniqueRoadmaps = [];
for (const roadmap of roadmaps) {
if (!uniqueRoadmaps.includes(roadmap)) {
uniqueRoadmaps.push(roadmap);
}
}
console.log(uniqueRoadmaps); // ['JavaScript', 'React', 'Node.js']
```

View File

@@ -0,0 +1,9 @@
No, the `forEach()` method does not return a new array. It simply calls a provided function on each element in the array.
```js
const roadmaps = ['JavaScript', 'React', 'Node.js'];
roadmaps.forEach((roadmap) => {
console.log(roadmap);
});
```

View File

@@ -0,0 +1,20 @@
The Head and Stack in JavaScript Engine are two different data structures that store data in different ways.
## Stack
The Stack is a small, organized region of memory. It is where primitive values, function calls, and local variables are stored. It follows a "Last In, First Out" (LIFO) order, meaning that the last item added to the stack is the first one to be removed. Each function invocation creates a new stack frame, which contains the function's local variables, return address, and other contextual data.
## Heap
The Heap is a large, mostly unstructured region of memory. It is where `objects`, `arrays`, and `functions` are stored. Variables from the Stack (e.g., in functions) point to locations in the Heap for these dynamically allocated structures.
When you declare a primitive type (like a number or boolean), it's usually managed in the stack. But when you create an object, array, or function, it's stored in the heap, and the stack will hold a reference to that location in the heap.
For example:
```js
const name = 'JavaScript'; // Stored on the stack
const roadmap = { name: 'JS' }; // `roadmap` reference on the stack, actual object { name: 'JS' } in the heap
```
In the code above, the primitive value `JavaScript` for variable `name` is directly stored on the stack. For the object assigned to `roadmap`, its actual data resides in the heap, and the reference to this data (a memory address pointer) is held on the stack.

View File

@@ -0,0 +1,16 @@
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. This means that no matter where the functions and variables are declared, they are moved to the top of their scope regardless of whether their scope is global or local. Note that hoisting only moves the declaration, not the initialization.
```js
console.log(x === undefined); // true
var x = 3;
console.log(x); // 3
```
The above code snippet can be visualized in the following way:
```js
var x;
console.log(x === undefined); // true
x = 3;
console.log(x); // 3
```

View File

@@ -0,0 +1,18 @@
The IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined.
```js
(function () {
console.log('Hello Roadmap!');
})();
```
The IIFE is frequently used to create a new scope to avoid variable hoisting from within blocks.
```js
(function () {
var roadmap = 'JavaScript';
console.log(roadmap);
})();
console.log(roadmap); // ReferenceError: name is not defined
```

View File

@@ -0,0 +1,12 @@
To make an object immutable, you can use `Object.freeze()` method. It prevents the modification of existing property values and prevents the addition of new properties.
```js
const roadmap = {
name: 'JavaScript',
};
Object.freeze(roadmap);
roadmap.name = 'JavaScript Roadmap'; // throws an error in strict mode
console.log(roadmap.name); // JavaScript
```

View File

@@ -0,0 +1,21 @@
As the name says, the increment operator increases the value of a variable by **1**. There are two types of increment operators: `pre-increment` and `post-increment`.
## Pre-increment
The pre-increment operator increases the value of a variable by 1 and then returns the value. For example:
```js
let x = 1;
console.log(++x); // 2
console.log(x); // 2
```
## Post-increment
The post-increment operator returns the value of a variable and then increases the value by 1. For example:
```js
let x = 1;
console.log(x++); // 1
console.log(x); // 2
```

View File

@@ -0,0 +1,21 @@
You can use the `while` or `for` loop to create an infinite loop.
## While loop
To create an infinite loop with the `while` loop, we can use the `true` keyword as the condition.
```js
while (true) {
// do something
}
```
## For loop
To create an infinite loop with the `for` loop, we can use the `true` keyword as the condition.
```js
for (let i = 0; true; i++) {
// do something
}
```

View File

@@ -0,0 +1,38 @@
Inheritance is a way to create a new `Class` from an existing `Class`. The new `Class` inherits all the properties and methods from the existing `Class`. The new `Class` is called the child `Class`, and the existing `Class` is called the parent `Class`.
## Example
```js
class Roadmap {
constructor(name, description, slug) {
this.name = name;
this.description = description;
this.slug = slug;
}
getRoadmapUrl() {
console.log(`https://roadmap.sh/${this.slug}`);
}
}
class JavaScript extends Roadmap {
constructor(name, description, slug) {
super(name, description, slug);
}
greet() {
console.log(`${this.name} - ${this.description}`);
}
}
const js = new JavaScript(
'JavaScript Roadmap',
'Learn JavaScript',
'javascript'
);
js.getRoadmapUrl(); // https://roadmap.sh/javascript
js.greet(); // JavaScript Roadmap - Learn JavaScript
```
In the above example, the `JavaScript` class inherits the `getRoadmapUrl()` method from the `Roadmap` class. This is because the `JavaScript` class extends the `Roadmap` class using the `extends` keyword. In the `JavaScript` class, the `getRoadmapUrl()` method is not found, so JavaScript looks up the prototype chain and finds the `getRoadmapUrl()` method in the `Roadmap` class.

View File

@@ -0,0 +1,15 @@
JavaScript label statements are used to prefix a label to an identifier. It can be used with `break` and `continue` statement to control the flow more precisely.
```js
loop1: for (let i = 0; i < 5; i++) {
if (i === 1) {
continue loop1; // skips the rest of the code in the loop1
}
console.log(`i: ${i}`);
}
// Output:
// i: 0
// i: 2
// i: 3
// i: 4
```

View File

@@ -0,0 +1,43 @@
There are four logical operators in JavaScript: `||` (OR), `&&` (AND), `!` (NOT), and `??` (Nullish Coalescing). They can be used with boolean values, or with non-boolean values.
## OR (||)
The OR operator (`||`) returns the first truthy value, or the last value if none are truthy.
```js
console.log('hello' || 0); // hello
console.log(false || 'hello'); // hello
console.log('hello' || 'world'); // hello
```
## AND (&&)
The AND operator (`&&`) aka logical conjunction returns the first falsy value, or the last value if none are falsy.
```js
console.log('hello' && 0); // 0
console.log(false && 'hello'); // false
console.log('hello' && 'world'); // world
```
## NOT (!)
It simply inverts the boolean value of its operand.
```js
console.log(!true); // false
console.log(!false); // true
console.log(!'hello'); // false
console.log(!0); // true
```
## Nullish Coalescing (??)
The Nullish Coalescing Operator (`??`) returns the right operand if the left one is `null` or `undefined`, otherwise, it returns the left operand. It's useful for setting default values without considering falsy values like `0` or `''` as absent.
```js
console.log(null ?? 'hello'); // hello
console.log(undefined ?? 'hello'); // hello
console.log('' ?? 'hello'); // ''
console.log(0 ?? 'hello'); // 0
```

View File

@@ -0,0 +1,12 @@
No, the `map()` method does not mutate the original array. It returns a new array with the results of calling a provided function on every element in the calling array.
```js
const roadmaps = ['JavaScript', 'React', 'Node.js'];
const renamedRoadmaps = roadmaps.map((roadmap) => {
return `${roadmap} Roadmap`;
});
console.log(roadmaps); // ['JavaScript', 'React', 'Node.js']
console.log(renamedRoadmaps); // ['JavaScript Roadmap', 'React Roadmap', 'Node.js Roadmap']
```

View File

@@ -0,0 +1,19 @@
Map is another data structure in JavaScript which is similar to `Object` but the key can be of any type. It is a collection of elements where each element is stored as a Key, value pair. It is also known as a Hash table or a dictionary.
The `key` can be of any type but the `value` can be of any type. The `key` is unique and immutable, whereas the `value` can be mutable or immutable.
```js
const roadmap = new Map();
roadmap.set('name', 'JavaScript');
roadmap.set('type', 'dynamic');
roadmap.set('year', 1995);
console.log(roadmap.get('name')); // JavaScript
roadmap.delete('year');
console.log(roadmap.has('year')); // false
console.log(roadmap.size); // 2
roadmap.clear();
console.log(roadmap.size); // 0
```

View File

@@ -0,0 +1,8 @@
You can use `getBoundingClientRect` method to get the dimensions of an element.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const dimensions = roadmapWrapper.getBoundingClientRect();
console.log(dimensions); // DOMRect { x: 8, y: 8, width: 784, height: 784, top: 8, right: 792, bottom: 792, left: 8 }
```

View File

@@ -0,0 +1,25 @@
Yes, you can merge multiple arrays into one array using the `concat()` method, or the spread operator `...`.
## concat()
The `concat()` method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
```js
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = arr1.concat(arr2);
console.log(arr3); // [1, 2, 3, 4, 5, 6]
```
## Spread operator
The spread operator `...` is used to expand an iterable object into the list of arguments.
```js
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2];
console.log(arr3); // [1, 2, 3, 4, 5, 6]
```

View File

@@ -0,0 +1,8 @@
The Nullish Coalescing Operator (`??`) returns the right operand if the left one is `null` or `undefined`, otherwise, it returns the left operand. It's useful for setting default values without considering falsy values like `0` or `''` as absent.
```js
console.log(null ?? 'hello'); // hello
console.log(undefined ?? 'hello'); // hello
console.log('' ?? 'hello'); // ''
console.log(0 ?? 'hello'); // 0
```

View File

@@ -0,0 +1,9 @@
In order to parse JSON, you can use the `JSON.parse()` method. It parses a JSON string and returns the JavaScript equivalent.
```js
const json = '{"name":"JavaScript","year":1995}';
const roadmap = JSON.parse(json);
console.log(roadmap.name); // JavaScript
console.log(roadmap.year); // 1995
```

View File

@@ -0,0 +1,10 @@
The `event.preventDefault()` method is used to prevent the default action of an event. For example, when you click on a link, the default action is to navigate to the link's URL. But, if you want to prevent the navigation, you can use `event.preventDefault()` method.
```js
const link = document.querySelector('a');
link.addEventListener('click', (event) => {
event.preventDefault();
console.log('Clicked on link!');
});
```

View File

@@ -0,0 +1,51 @@
The core difference between `Promise.all()` and `Promise.allSettled()` is that `Promise.all()` rejects immediately if any of the promises reject whereas `Promise.allSettled()` waits for all of the promises to settle (either resolve or reject) and then returns the result.
## Initialize
```js
const promise1 = Promise.resolve('Promise 1 resolved');
const promise2 = Promise.reject('Promise 2 rejected');
```
## Using `Promise.all()`
```js
Promise.all([promise1, promise2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.log('An error occurred in Promise.all():', error);
});
// Output:
// An error occurred in Promise.all(): Promise 2 rejected
```
In the above code, the `Promise.all()` rejects immediately when any of the `promise2` rejects.
## Using `Promise.allSettled()`
```js
Promise.allSettled([promise1, promise2]).then((results) => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(
`Promise ${index + 1} was fulfilled with value:`,
result.value
);
} else {
console.log(
`Promise ${index + 1} was rejected with reason:`,
result.reason
);
}
});
});
// Output:
// Promise 1 was fulfilled with value: Promise 1 resolved
// Promise 2 was rejected with reason: Promise 2 rejected
```
In the above code, the `Promise.allSettled()` waits for all of the promises to settle (either resolve or reject) and then returns the result.

View File

@@ -0,0 +1,27 @@
The prototype chain in JavaScript refers to the chain of objects linked by their prototypes. When a property or method is accessed on an object, JavaScript first checks the object itself. If it doesn't find it there, it looks up the property or method in the object's prototype. This process continues, moving up the chain from one prototype to the next, until the property or method is found or the end of the chain is reached (typically the prototype of the base object, which is `null`). The prototype chain is fundamental to JavaScript's prototypal inheritance model, allowing objects to inherit properties and methods from other objects.
## Example
```js
const roadmap = {
getRoadmapUrl() {
console.log(`https://roadmap.sh/${this.slug}`);
},
};
const javascript = {
name: 'JavaScript Roadmap',
description: 'Learn JavaScript',
slug: 'javascript',
greet() {
console.log(`${this.name} - ${this.description}`);
},
};
Object.setPrototypeOf(javascript, roadmap); // or javascript.__proto__ = roadmap;
javascript.getRoadmapUrl(); // https://roadmap.sh/javascript
javascript.greet(); // JavaScript Roadmap - Learn JavaScript
```
In the above example, the `javascript` object inherits the `getRoadmapUrl()` method from the `roadmap` object. This is because the `javascript` object's prototype is set to the `roadmap` object using the `Object.setPrototypeOf()` method. In the `javascript` object, the `getRoadmapUrl()` method is not found, so JavaScript looks up the prototype chain and finds the `getRoadmapUrl()` method in the `roadmap` object.

View File

@@ -0,0 +1,18 @@
For selecting elements in the DOM, the `querySelector` and `querySelectorAll` methods are the most commonly used. They are both methods of the `document` object, and they both accept a CSS selector as an argument.
## querySelector
The `querySelector` method returns the first element that matches the specified selector. If no matches are found, it returns `null`.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmapTitle = document.querySelector('#roadmap-title');
```
## querySelectorAll
The `querySelectorAll` method returns a `NodeList` of all elements that match the specified selector. If no matches are found, it returns an empty `NodeList`.
```js
const roadmapItems = document.querySelectorAll('.roadmap-item');
```

View File

@@ -0,0 +1,24 @@
You can use the `reduce()` method to reduce an array to a single value. The `reduce()` method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.
## Syntax
```js
array.reduce((accumulator, currentValue) => {
// ...
}, initialValue);
```
## Example
You can use the `reduce()` method to sum all the numbers in an array.
```js
const numbers = [1, 2, 3, 4, 5, 6];
const sum = numbers.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);
console.log(numbers); // [1, 2, 3, 4, 5, 6]
console.log(sum); // 21
```

View File

@@ -0,0 +1,9 @@
To remove a DOM element, you can use the `remove` or `removeChild` method of the `Node` interface.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmapTitle = document.querySelector('#roadmap-title');
roadmapWrapper.removeChild(roadmapTitle);
roadmapWrapper.remove();
```

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