mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-31 21:11:44 +02:00
Allow creating personal version of frontend roadmap (#4627)
* Create Roadmap Version * Change button position * Update frontend JSON * Remove `topicCount` * Add fork at title * Update UI for create your own version * Add functionality to load your own version * Load user version of roadmap * Update forkable roadmap --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
147
src/components/CreateVersion/CreateVersion.tsx
Normal file
147
src/components/CreateVersion/CreateVersion.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { httpGet, httpPost } from '../../lib/http';
|
||||||
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { isLoggedIn } from '../../lib/jwt';
|
||||||
|
import { GitFork, Layers2, Loader2, Map } from 'lucide-react';
|
||||||
|
import { showLoginPopup } from '../../lib/popup';
|
||||||
|
import type { RoadmapDocument } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||||
|
|
||||||
|
type CreateVersionProps = {
|
||||||
|
roadmapId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CreateVersion(props: CreateVersionProps) {
|
||||||
|
const { roadmapId } = props;
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
const [userVersion, setUserVersion] = useState<RoadmapDocument>();
|
||||||
|
|
||||||
|
async function loadMyVersion() {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const { response, error } = await httpGet<RoadmapDocument>(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-get-my-version/${roadmapId}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
setUserVersion(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadMyVersion().finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function createVersion() {
|
||||||
|
if (isCreating || !roadmapId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
showLoginPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
|
const { response, error } = await httpPost<{ roadmapId: string }>(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-create-version/${roadmapId}`,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
setIsCreating(false);
|
||||||
|
toast.error(error?.message || 'Failed to create version');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roadmapEditorUrl = `${
|
||||||
|
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||||
|
}/${response?.roadmapId}`;
|
||||||
|
|
||||||
|
window.open(roadmapEditorUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="h-[30px] w-[206px] animate-pulse rounded-md bg-gray-300"></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLoading && userVersion?._id) {
|
||||||
|
return (
|
||||||
|
<div className={'flex items-center'}>
|
||||||
|
<a
|
||||||
|
href={`/r?id=${userVersion._id}`}
|
||||||
|
className="flex items-center rounded-md border border-blue-400 bg-gray-50 px-2.5 py-1 text-xs font-medium text-blue-600 hover:bg-blue-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||||
|
>
|
||||||
|
<Map size="15px" className="mr-1.5" />
|
||||||
|
Visit your own version
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfirming) {
|
||||||
|
return (
|
||||||
|
<p className="flex h-[30px] items-center text-sm text-red-500">
|
||||||
|
Create and edit a custom roadmap from this?
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsConfirming(false);
|
||||||
|
createVersion().finally(() => null);
|
||||||
|
}}
|
||||||
|
className="ml-2 font-semibold underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<span className="text-xs"> / </span>
|
||||||
|
<button
|
||||||
|
className="font-semibold underline underline-offset-2"
|
||||||
|
onClick={() => setIsConfirming(false)}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={isCreating}
|
||||||
|
className="flex items-center justify-center rounded-md border border-gray-300 bg-gray-50 px-2.5 py-1 text-xs font-medium text-black hover:bg-gray-200 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
showLoginPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsConfirming(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCreating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-3 w-3 animate-spin stroke-[2.5]" />
|
||||||
|
Please wait ..
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<GitFork className="mr-1.5" size="16px" />
|
||||||
|
Create your own Version
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
@@ -175,26 +175,26 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<a
|
|
||||||
href={`/r?id=${roadmap._id}`}
|
|
||||||
className={
|
|
||||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none'
|
|
||||||
}
|
|
||||||
target={'_blank'}
|
|
||||||
>
|
|
||||||
<ExternalLink className="inline-block h-4 w-4" />
|
|
||||||
Visit
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
href={editorLink}
|
href={editorLink}
|
||||||
className={
|
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'
|
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-none'
|
||||||
}
|
}
|
||||||
target={'_blank'}
|
target={'_blank'}
|
||||||
>
|
>
|
||||||
<PenSquare className="inline-block h-4 w-4" />
|
<PenSquare className="inline-block h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href={`/r?id=${roadmap._id}`}
|
||||||
|
className={
|
||||||
|
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs hover:bg-blue-50 focus:outline-none text-blue-600'
|
||||||
|
}
|
||||||
|
target={'_blank'}
|
||||||
|
>
|
||||||
|
<ExternalLink className="inline-block h-4 w-4" />
|
||||||
|
Visit
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
@@ -8,7 +8,8 @@ import YouTubeAlert from './YouTubeAlert.astro';
|
|||||||
import ProgressHelpPopup from './ProgressHelpPopup.astro';
|
import ProgressHelpPopup from './ProgressHelpPopup.astro';
|
||||||
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||||
import { TeamVersions } from './TeamVersions/TeamVersions';
|
import { TeamVersions } from './TeamVersions/TeamVersions';
|
||||||
import { RoadmapFrontmatter } from '../lib/roadmap';
|
import { CreateVersion } from './CreateVersion/CreateVersion';
|
||||||
|
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -20,6 +21,7 @@ export interface Props {
|
|||||||
hasSearch?: boolean;
|
hasSearch?: boolean;
|
||||||
question?: RoadmapFrontmatter['question'];
|
question?: RoadmapFrontmatter['question'];
|
||||||
hasTopics?: boolean;
|
hasTopics?: boolean;
|
||||||
|
isForkable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -32,6 +34,7 @@ const {
|
|||||||
note,
|
note,
|
||||||
hasTopics = false,
|
hasTopics = false,
|
||||||
question,
|
question,
|
||||||
|
isForkable = false,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const isRoadmapReady = !isUpcoming;
|
const isRoadmapReady = !isUpcoming;
|
||||||
@@ -58,13 +61,21 @@ const hasTnsBanner = !!tnsBannerLink;
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div class='mb-3 mt-0 sm:mb-4'>
|
<div class='mb-3 mt-0 sm:mb-4'>
|
||||||
|
{
|
||||||
|
isForkable && (
|
||||||
|
<div class='mb-2'>
|
||||||
|
<CreateVersion client:load roadmapId={roadmapId} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
|
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
|
||||||
{title}
|
{title}
|
||||||
<span class='relative top-0 sm:-top-1'>
|
<span class='relative top-0 sm:-top-1'>
|
||||||
<MarkFavorite
|
<MarkFavorite
|
||||||
resourceId={roadmapId}
|
resourceId={roadmapId}
|
||||||
resourceType='roadmap'
|
resourceType='roadmap'
|
||||||
className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-4 sm:[&>svg]:w-4 ml-1.5 relative focus:outline-0'
|
className='relative ml-1.5 text-gray-500 !opacity-100 hover:text-gray-600 focus:outline-0 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:stroke-gray-400 [&>svg]:stroke-[0.4] hover:[&>svg]:stroke-gray-600 sm:[&>svg]:h-4 sm:[&>svg]:w-4'
|
||||||
client:only='react'
|
client:only='react'
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
5743
src/data/roadmaps/frontend/frontend-forkable.json
Normal file
5743
src/data/roadmaps/frontend/frontend-forkable.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,13 @@ briefDescription: 'Step by step guide to becoming a frontend developer in 2023'
|
|||||||
title: 'Frontend Developer'
|
title: 'Frontend Developer'
|
||||||
description: 'Step by step guide to becoming a modern frontend developer in 2023'
|
description: 'Step by step guide to becoming a modern frontend developer in 2023'
|
||||||
hasTopics: true
|
hasTopics: true
|
||||||
|
isForkable: true
|
||||||
tnsBannerLink: 'https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
|
tnsBannerLink: 'https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
|
||||||
question:
|
question:
|
||||||
title: 'What is Frontend Development?'
|
title: 'What is Frontend Development?'
|
||||||
description: |
|
description: |
|
||||||
Front-end development is the development of visual and interactive elements of a website that users interact with directly. It's a combination of HTML, CSS and [JavaScript](/javascript), where HTML provides the structure, CSS the styling and layout, and JavaScript the dynamic behaviour and interactivity.
|
Front-end development is the development of visual and interactive elements of a website that users interact with directly. It's a combination of HTML, CSS and [JavaScript](/javascript), where HTML provides the structure, CSS the styling and layout, and JavaScript the dynamic behaviour and interactivity.
|
||||||
|
|
||||||
## What does a Frontend Developer do?
|
## What does a Frontend Developer do?
|
||||||
As a front-end developer, you'll be responsible for creating the user interface of a website, to ensure it looks good and is easy to use, with great focus on design principles and user experience. You'll be working closely with designers, back-end developers, and project managers to make sure the final product meets the client's needs and provides the best possible experience for the end-users.
|
As a front-end developer, you'll be responsible for creating the user interface of a website, to ensure it looks good and is easy to use, with great focus on design principles and user experience. You'll be working closely with designers, back-end developers, and project managers to make sure the final product meets the client's needs and provides the best possible experience for the end-users.
|
||||||
dimensions:
|
dimensions:
|
||||||
|
@@ -8,6 +8,7 @@ export interface RoadmapFrontmatter {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
hasTopics: boolean;
|
hasTopics: boolean;
|
||||||
|
isForkable: boolean;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
isUpcoming: boolean;
|
isUpcoming: boolean;
|
||||||
tnsBannerLink?: string;
|
tnsBannerLink?: string;
|
||||||
|
@@ -86,6 +86,7 @@ if (roadmapFAQs.length) {
|
|||||||
roadmapId={roadmapId}
|
roadmapId={roadmapId}
|
||||||
hasTopics={roadmapData.hasTopics}
|
hasTopics={roadmapData.hasTopics}
|
||||||
isUpcoming={roadmapData.isUpcoming}
|
isUpcoming={roadmapData.isUpcoming}
|
||||||
|
isForkable={roadmapData.isForkable}
|
||||||
question={roadmapData.question}
|
question={roadmapData.question}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user