feat: showcase roadmap (#7791)

* wip

* wip

* fix: status issue

* feat: update UI

* wip

* wip: showcase status

* wip: showcase listing

* feat: update showcase status

* chore: update roadmap content json (#7738)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* Fix issue in sticky top ad

* Add preloading of ad image

* feat(backend): update unit testing node resources (#7743)

* feat: container orchestration

* Update container-orchestration@Yq8kVoRf20aL_o4VZU5--.md

Simplified content and added working links to resources for better clarity and learning.

* Update container-orchestration@Yq8kVoRf20aL_o4VZU5--.md

Replace content and added working links to resources for better clarity and learning.

* Update container-orchestration@Yq8kVoRf20aL_o4VZU5--.md

* Update src/data/roadmaps/devops/content/container-orchestration@Yq8kVoRf20aL_o4VZU5--.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>

* docs: fix typos and improve grammar in documentation (#7747)

Corrects typos and grammatical errors in various markdown files to enhance clarity and readability.

* feat: add PearAI code editor

Added PearAI to the list of AI Code Editors (An Open Source Option for developers!)

* chore: update roadmap content json (#7751)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* feat: center of mass explain video (#7754)

video addition explaining COM better

* Ad new changelog entry

* Update C# link to correct URL (#7757)

* Add engineering manager roadmap

* chore: update roadmap content json (#7758)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* Update frontend FAQs (#7764)

Tweaked the first two Qs

* Update DevOps skills (#7763)

Added internal refs

* Add ref to DevOps roadmap in guide (#7762)

Added roadmap ref.

* Add engineering manager roadmap

* Update engineering manager roadmap content

* Update engineering manager roadmap

* Add content to engineering manager roadmap

* chore: update roadmap content json (#7768)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* fix: postgresql link (#7766)

* fix(typo): comma todo-list-api.md (#7772)

* Add new link of Redis in FullStack (#7771)

* chore: update roadmap content json (#7778)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* Add content to vue.js performance (#7777)

* Update performance@f7N4pAp_jBlT8_8owAcbG.md

* Update src/data/roadmaps/vue/content/performance@f7N4pAp_jBlT8_8owAcbG.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>

* Update resources for Authentication (#7745)

* Update authentication-vs-authorization@WG7DdsxESm31VcLFfkVTz.md

replaced a wrong article with one about biometrics

* Update understand-common-exploit-frameworks@Lg7mz4zeCToEzZBFxYuaU.md

link redirects to a Thai gambling game site

* Add resource for rest-assured (#7737)

## Content

I’ve added a beginner-friendly article, A Guide to REST-assured, from Baeldung to the REST Assured section. If there’s anything that doesn’t meet the format, please feel free to comment. Thanks😊.

## Issue
Fixed #7736

* Add UX design resource (#7710)

* Update conceptual-design@r6D07cN0Mg4YXsiRSrl1_.md

I have added an article by Dan Nessler on How to apply a design thinking, HCD, UX or any creative process from scratch which is a how-to article aims at providing designers, creative thinkers or even project managers with a tool to set up, frame, organise, structure, run or manage design challenges, and projects: The Double Diamond revamped.

* Update conceptual-design@r6D07cN0Mg4YXsiRSrl1_.md

I have added an article by Dan Nessler on How to apply a design thinking, HCD, UX or any creative process from scratch which is a how-to article aims at providing designers, creative thinkers or even project managers with a tool to set up, frame, organise, structure, run or manage design challenges, and projects: The Double Diamond revamped.

* Add user personas resource to UX design (#7709)

Added more resources from IxD Foundation and NN group.

Co-authored-by: Shivam Kumar <85393390+TinyTijil@users.noreply.github.com>

* Add linked in content (#7695)

* Update linkedin@6UR59TigEZ0NaixbaUIqn.md

* Update src/data/roadmaps/devrel/content/linkedin@6UR59TigEZ0NaixbaUIqn.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>

* Added article on AuthN vs AuthZ (#7694)

Added a guide on the difference between authentication and authorization, since these terms are often confused.

* Add a video to the Decentralization section (#7692)

* adding a video to the Decentralization section

* adding a video to the Decentralization section

* Fixes typo in 104-proc-priorities.md (#7684)

Old: renice +5
New: renice -5

From my research, after reading the topic in the Linux roadmap, it didnt make sense that increasing the priority of a process was made by +5, the topic said that a negative number makes the priority higher, so do many articles on the internet.

* Add bastion host and file integrity checker idea

* Add pomodoro timer project idea

* Add project idea for quiz app

* chore: update roadmap content json (#7785)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* fix typo in dockerhub alternatives (#7780)

Co-authored-by: Fabio Stabile <fabio.stabile@mia-platform.eu>

* Add content to engineering manager roadmap (#7779)

* Update system-design-and-architecture@iX4HPgoiEbc_gze1A01n4.md

* Update src/data/roadmaps/engineering-manager/content/system-design-and-architecture@iX4HPgoiEbc_gze1A01n4.md

* Update src/data/roadmaps/engineering-manager/content/system-design-and-architecture@iX4HPgoiEbc_gze1A01n4.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>

* Add project idea for stories feature

* Add weather app project idea

* Update project ideas

* Add engineering manager roadmap content

* Update engineering manager roadmap content

* Add DevOps best practices guide

* Add AI Engineer introduction video (#7788)

* Added Introduction Video

* Changed formatting

* Update src/data/roadmaps/ai-engineer/content/introduction@_hYN0gEi9BL24nptEtXWU.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>

* chore: update roadmap content json (#7789)

Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>

* Add devops automation tools

* Add featuring functionality

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: kamranahmedse <4921183+kamranahmedse@users.noreply.github.com>
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
Co-authored-by: Rogério Ferreira de Souza <rogeriofrsouza@gmail.com>
Co-authored-by: Jawher Kl <kalleljawher4@gmail.com>
Co-authored-by: garyellow <gary20011110@gmail.com>
Co-authored-by: Nang <nathanang2000@gmail.com>
Co-authored-by: FormerlyWD <156501761+FormerlyWD@users.noreply.github.com>
Co-authored-by: dudi vaichledere <117650526+dudi-w@users.noreply.github.com>
Co-authored-by: Ed Lan <165309301+Edlan01@users.noreply.github.com>
Co-authored-by: elias_sisay <87943132+eliassisay@users.noreply.github.com>
Co-authored-by: feelsgoodfrog <gudrb963@gmail.com>
Co-authored-by: Gustavo Martins Pereira <gustavo.martins.pereira.main@gmail.com>
Co-authored-by: Maksymilian <maxsapa@gmail.com>
Co-authored-by: b4haa7 <69992780+88BahaaAdel88@users.noreply.github.com>
Co-authored-by: Wick Dynex <1328032567@qq.com>
Co-authored-by: Shivam Kumar <85393390+kshivam14@users.noreply.github.com>
Co-authored-by: Shivam Kumar <85393390+TinyTijil@users.noreply.github.com>
Co-authored-by: Yanbo Wang <yanbotravelaroundworld@gmail.com>
Co-authored-by: Lisa Dziuba <lisa@flawlessapp.io>
Co-authored-by: Karamoko Israël Abdelaziz Axel <72276211+karamokoisrael@users.noreply.github.com>
Co-authored-by: duds <xaviduds@gmail.com>
Co-authored-by: Fabio Stabile <93452841+fabioS24@users.noreply.github.com>
Co-authored-by: Fabio Stabile <fabio.stabile@mia-platform.eu>
Co-authored-by: Naresh Thakur <122244033+thinklikeacto@users.noreply.github.com>
Co-authored-by: Gustaf <79180496+GGyll@users.noreply.github.com>
This commit is contained in:
Arik Chakma
2024-11-27 12:07:59 +05:00
committed by GitHub
co-authored by Shivam Kumar Kamran Ahmed kamranahmedse Fabio Stabile github-actions[bot] Rogério Ferreira de Souza Jawher Kl garyellow Nang FormerlyWD dudi vaichledere Ed Lan elias_sisay feelsgoodfrog Gustavo Martins Pereira Maksymilian b4haa7 Wick Dynex Shivam Kumar Yanbo Wang Lisa Dziuba Karamoko Israël Abdelaziz Axel duds Fabio Stabile Naresh Thakur Gustaf
parent ee95280452
commit 43849e758e
13 changed files with 609 additions and 167 deletions
@@ -23,11 +23,16 @@ export const allowedCustomRoadmapType = ['role', 'skill'] as const;
export type AllowedCustomRoadmapType =
(typeof allowedCustomRoadmapType)[number];
export const allowedShowcaseStatus = ['visible', 'hidden'] as const;
export const allowedShowcaseStatus = [
'submitted',
'approved',
'rejected',
'rejected_with_reason',
] as const;
export type AllowedShowcaseStatus = (typeof allowedShowcaseStatus)[number];
export interface RoadmapDocument {
_id?: string;
_id: string;
title: string;
description?: string;
slug?: string;
@@ -51,14 +56,22 @@ export interface RoadmapDocument {
edges: any[];
isDiscoverable?: boolean;
showcaseStatus?: AllowedShowcaseStatus;
ratings: {
average: number;
totalCount: number;
breakdown: {
[key: number]: number;
};
};
showcaseStatus?: AllowedShowcaseStatus;
showcaseRejectedReason?: string;
showcaseRejectedAt?: Date;
showcaseSubmittedAt?: Date;
showcaseApprovedAt?: Date;
hasMigratedContent?: boolean;
createdAt: Date;
updatedAt: Date;
}
+16 -32
View File
@@ -1,12 +1,15 @@
import { useEffect, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import { type AppError, type FetchError, httpGet } from '../../lib/http';
import { RoadmapHeader } from './RoadmapHeader';
import { TopicDetail } from '../TopicDetail/TopicDetail';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { currentRoadmap } from '../../stores/roadmap';
import { RestrictedPage } from './RestrictedPage';
import { FlowRoadmapRenderer } from './FlowRoadmapRenderer';
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpGet, type FetchError } from '../../lib/query-http';
import { useCustomRoadmap } from '../../hooks/use-custom-roadmap';
export const allowedLinkTypes = [
'video',
@@ -71,43 +74,24 @@ export function CustomRoadmap(props: CustomRoadmapProps) {
const [isLoading, setIsLoading] = useState(true);
const [roadmap, setRoadmap] = useState<GetRoadmapResponse | null>(null);
const [error, setError] = useState<AppError | FetchError | undefined>();
async function getRoadmap() {
setIsLoading(true);
const { data, error } = useCustomRoadmap({
id,
secret,
slug,
});
const roadmapUrl = slug
? new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap-by-slug/${slug}`,
)
: new URL(`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`);
if (secret) {
roadmapUrl.searchParams.set('secret', secret);
}
const { response, error } = await httpGet<GetRoadmapResponse>(
roadmapUrl.toString(),
);
if (error || !response) {
setError(error);
setIsLoading(false);
useEffect(() => {
if (!data) {
return;
}
document.title = `${response.title} - roadmap.sh`;
setRoadmap(response);
currentRoadmap.set(response);
document.title = `${data.title} - roadmap.sh`;
setRoadmap(data);
currentRoadmap.set(data);
setIsLoading(false);
}
useEffect(() => {
getRoadmap().finally(() => {
hideRoadmapLoader();
});
}, []);
hideRoadmapLoader();
}, [data]);
if (isLoading) {
return null;
+118 -106
View File
@@ -11,6 +11,8 @@ import { RoadmapActionButton } from './RoadmapActionButton';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
import { CustomRoadmapAlert } from './CustomRoadmapAlert.tsx';
import { CustomRoadmapRatings } from './CustomRoadmapRatings.tsx';
import { ShowcaseStatus } from './Showcase/ShowcaseStatus.tsx';
import { ShowcaseAlert } from './Showcase/ShowcaseAlert.tsx';
type RoadmapHeaderProps = {};
@@ -73,122 +75,132 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
: '/images/default-avatar.png';
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
{!$canManageCurrentRoadmap && <CustomRoadmapAlert />}
{creator?.name && (
<div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500">
<img
alt={creator.name}
src={avatarUrl}
className="h-5 w-5 rounded-full"
/>
<span>
Created by&nbsp;
<span className="font-semibold text-gray-900">
{creator?.name}
</span>
{team && (
<>
&nbsp;from&nbsp;
<span className="font-semibold text-gray-900">
{team?.name}
</span>
</>
)}
</span>
</div>
<>
<div className="relative border-b">
{$currentRoadmap && $canManageCurrentRoadmap && (
<ShowcaseAlert currentRoadmap={$currentRoadmap} />
)}
<div className="mb-3 mt-4 sm:mb-4">
<h1 className="text-2xl font-bold sm:mb-2 sm:text-4xl">{title}</h1>
<p className="mt-0.5 text-sm text-gray-500 sm:text-lg">
{description}
</p>
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="flex justify-stretch gap-1 sm:gap-2">
<a
href="/community"
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label="Back to All Roadmaps"
>
&larr;
<span className="hidden sm:inline">&nbsp;Discover more</span>
</a>
<div className="container relative py-5 sm:py-12">
{!$canManageCurrentRoadmap && <CustomRoadmapAlert />}
<ShareRoadmapButton
roadmapId={roadmapId!}
description={description!}
pageUrl={`https://roadmap.sh/r/${roadmapSlug}`}
allowEmbed={true}
/>
{creator?.name && (
<div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500">
<img
alt={creator.name}
src={avatarUrl}
className="h-5 w-5 rounded-full"
/>
<span>
Created by&nbsp;
<span className="font-semibold text-gray-900">
{creator?.name}
</span>
{team && (
<>
&nbsp;from&nbsp;
<span className="font-semibold text-gray-900">
{team?.name}
</span>
</>
)}
</span>
</div>
)}
<div className="mb-3 mt-4 sm:mb-4">
<h1 className="text-2xl font-bold sm:mb-2 sm:text-4xl">{title}</h1>
<p className="mt-0.5 text-sm text-gray-500 sm:text-lg">
{description}
</p>
</div>
<div className="flex items-center gap-2">
{$canManageCurrentRoadmap && (
<>
{isSharing && $currentRoadmap && (
<ShareOptionsModal
roadmapSlug={$currentRoadmap?.slug}
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,
});
<div className="flex justify-between gap-2 sm:gap-0">
<div className="flex justify-stretch gap-1 sm:gap-2">
<a
href="/community"
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label="Back to All Roadmaps"
>
&larr;
<span className="hidden sm:inline">&nbsp;Discover more</span>
</a>
<ShareRoadmapButton
roadmapId={roadmapId!}
description={description!}
pageUrl={`https://roadmap.sh/r/${roadmapSlug}`}
allowEmbed={true}
/>
</div>
<div className="flex items-center gap-2">
{$canManageCurrentRoadmap && (
<>
{isSharing && $currentRoadmap && (
<ShareOptionsModal
roadmapSlug={$currentRoadmap?.slug}
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,
});
}}
/>
)}
{$currentRoadmap && (
<ShowcaseStatus currentRoadmap={$currentRoadmap} />
)}
<RoadmapActionButton
onUpdateSharing={() => setIsSharing(true)}
onCustomize={() => {
window.location.href = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${$currentRoadmap?._id}`;
}}
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?',
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
/>
)}
</>
)}
<RoadmapActionButton
onUpdateSharing={() => setIsSharing(true)}
onCustomize={() => {
window.location.href = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${$currentRoadmap?._id}`;
}}
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?',
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
{showcaseStatus === 'approved' && (
<CustomRoadmapRatings
roadmapSlug={roadmapSlug!}
ratings={ratings!}
canManage={$canManageCurrentRoadmap}
unseenRatingCount={unseenRatingCount || 0}
/>
</>
)}
{((ratings?.average || 0) > 0 || showcaseStatus === 'visible') && (
<CustomRoadmapRatings
roadmapSlug={roadmapSlug!}
ratings={ratings!}
canManage={$canManageCurrentRoadmap}
unseenRatingCount={unseenRatingCount || 0}
/>
)}
)}
</div>
</div>
</div>
<RoadmapHint
roadmapTitle={title!}
hasTNSBanner={false}
roadmapId={roadmapId!}
/>
<RoadmapHint
roadmapTitle={title!}
hasTNSBanner={false}
roadmapId={roadmapId!}
/>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,88 @@
import { EyeIcon, FlagIcon, FrownIcon, SmileIcon } from 'lucide-react';
import { cn } from '../../../lib/classname';
import type { GetRoadmapResponse } from '../CustomRoadmap';
import { useState } from 'react';
import { SubmitShowcaseWarning } from './SubmitShowcaseWarning';
type ShowcaseAlertProps = {
currentRoadmap: GetRoadmapResponse;
};
export function ShowcaseAlert(props: ShowcaseAlertProps) {
const { currentRoadmap } = props;
const [showRejectedReason, setShowRejectedReason] = useState(false);
const { showcaseStatus } = currentRoadmap;
if (!showcaseStatus) {
return null;
}
const showcaseStatusMap = {
submitted: {
icon: EyeIcon,
label:
'We are currently reviewing your roadmap, please wait for our response.',
className: 'bg-blue-100 text-blue-600 border-blue-200',
},
approved: {
icon: SmileIcon,
label: 'Hooray! Your roadmap is now visible on the community page.',
className: 'text-green-600 bg-green-100 border-green-300',
},
rejected: {
icon: FrownIcon,
label: 'Sorry, we are unable to feature your roadmap at this time.',
className: 'text-red-600 bg-red-100 border-red-300',
},
rejected_with_reason: {
icon: FlagIcon,
label: (
<>
Your roadmap could not be featured at this time{' '}
<button
className="font-medium underline underline-offset-2 hover:text-red-800"
onClick={() => {
setShowRejectedReason(true);
}}
>
click here to see why
</button>
</>
),
className: 'text-red-800 bg-red-200 border-red-200',
},
};
const showcaseStatusDetails = showcaseStatusMap[showcaseStatus];
if (!showcaseStatusDetails) {
return null;
}
const { icon: Icon, label, className } = showcaseStatusDetails;
return (
<>
{showRejectedReason && (
<SubmitShowcaseWarning
onClose={() => {
setShowRejectedReason(false);
}}
/>
)}
<div
className={cn(
'z-10 border-b -mb-4',
showcaseStatusDetails.className,
)}
>
<div className="container relative flex items-center justify-center py-2 text-sm">
<div className={cn('flex items-center gap-2', className)}>
<Icon className="h-4 w-4 shrink-0 stroke-[2.5]" />
<div>{label}</div>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { SubmitShowcaseWarning } from './SubmitShowcaseWarning';
import type { GetRoadmapResponse } from '../CustomRoadmap';
import { SendIcon } from 'lucide-react';
type ShowcaseStatusProps = {
currentRoadmap: GetRoadmapResponse;
};
export function ShowcaseStatus(props: ShowcaseStatusProps) {
const { currentRoadmap } = props;
const { showcaseStatus } = currentRoadmap;
const [showSubmitWarning, setShowSubmitWarning] = useState(false);
if (!currentRoadmap || showcaseStatus) {
return null;
}
return (
<>
{showSubmitWarning && (
<SubmitShowcaseWarning
onClose={() => {
setShowSubmitWarning(false);
}}
/>
)}
<button
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:pl-1.5 sm:pr-3 sm:text-sm"
onClick={() => {
setShowSubmitWarning(true);
}}
disabled={!!showcaseStatus}
>
<SendIcon className="mr-0 h-4 w-4 stroke-[2.5] sm:mr-1.5" />
<span className="hidden sm:inline">Apply to be Featured</span>
</button>
</>
);
}
@@ -0,0 +1,122 @@
import { useMutation } from '@tanstack/react-query';
import { Modal } from '../../Modal';
import { queryClient } from '../../../stores/query-client';
import { httpPost } from '../../../lib/query-http';
import { useStore } from '@nanostores/react';
import { currentRoadmap } from '../../../stores/roadmap';
import { useToast } from '../../../hooks/use-toast';
import { DateTime } from 'luxon';
type SubmitShowcaseWarningProps = {
onClose: () => void;
};
export function SubmitShowcaseWarning(props: SubmitShowcaseWarningProps) {
const { onClose } = props;
const toast = useToast();
const $currentRoadmap = useStore(currentRoadmap);
const submit = useMutation(
{
mutationFn: async () => {
return httpPost(`/v1-submit-for-showcase/${$currentRoadmap?._id}`, {});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['get-roadmap'],
});
onClose();
},
onError: (error) => {
toast.error(error?.message || 'Something went wrong');
},
},
queryClient,
);
const {
showcaseStatus,
showcaseRejectedReason,
showcaseRejectedAt,
updatedAt,
} = $currentRoadmap || {};
return (
<Modal onClose={onClose}>
<div className="p-4">
<h2 className="text-lg font-semibold">
{showcaseStatus === 'rejected_with_reason'
? 'Rejected with Reason'
: 'Feature Your Roadmap'}
</h2>
<p className="mt-2 text-sm">
{showcaseStatus === 'rejected_with_reason' && (
<>
<span
className={
'block rounded-md bg-red-100 px-2 py-1.5 text-red-700'
}
>
{showcaseRejectedReason}
</span>
<span className="block mt-3">
Feel free to make changes to your roadmap and resubmit.
</span>
</>
)}
{!showcaseStatus && (
<>
We will review your roadmap and if accepted, we will make it
public and show it on the community roadmap listing.{' '}
<span className="mt-4 block font-medium">
Are you sure to submit?
</span>
</>
)}
</p>
<div className="mt-4 grid grid-cols-2 gap-2">
<button
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center text-sm hover:bg-gray-300"
onClick={onClose}
disabled={submit.isPending}
>
Cancel
</button>
<button
className="w-full rounded-lg bg-gray-900 py-2 text-sm text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
disabled={submit.isPending}
onClick={() => {
const updatedAtDate =
updatedAt && DateTime.fromJSDate(new Date(updatedAt));
const showcaseRejectedAtDate =
showcaseRejectedAt &&
DateTime.fromJSDate(new Date(showcaseRejectedAt));
if (
showcaseRejectedAtDate &&
updatedAtDate &&
updatedAtDate < showcaseRejectedAtDate
) {
toast.error(
'You need to make changes to your roadmap before resubmitting.',
);
return;
}
submit.mutate();
}}
>
{submit.isPending
? 'Submitting...'
: showcaseStatus === 'rejected_with_reason'
? 'Resubmit'
: 'Submit'}
</button>
</div>
</div>
</Modal>
);
}
@@ -2,8 +2,8 @@ export function SkeletonRoadmapHeader() {
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
<div className="flex items-center gap-1.5">
<div className="h-4 w-4 animate-pulse rounded-full bg-gray-300" />
<div className="-mb-0.5 flex items-center gap-1.5">
<div className="h-5 w-5 animate-pulse rounded-full bg-gray-300" />
<div className="h-5 w-5/12 animate-pulse rounded-md bg-gray-200" />
</div>
<div className="mb-3 mt-4 sm:mb-4">
@@ -12,7 +12,7 @@ export function SkeletonRoadmapHeader() {
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className='flex gap-1 sm:gap-2'>
<div className="flex gap-1 sm:gap-2">
<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-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[85px]" />
</div>
@@ -27,9 +27,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
return (
<>
<div className="mb-2 mt-6 flex items-center justify-between gap-2">
<h2 className="text-xs uppercase text-gray-400">
My AI Roadmaps
</h2>
<h2 className="text-xs uppercase text-gray-400">My AI Roadmaps</h2>
<a
href="/ai/explore"
@@ -62,6 +60,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) {
<>
{roadmaps.map((roadmap) => (
<a
key={roadmap.id}
href={`/ai/${roadmap.slug}`}
className="relative truncate rounded-md border bg-white p-2.5 text-left text-sm shadow-sm hover:border-gray-400 hover:bg-gray-50"
>
@@ -133,14 +133,6 @@ export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) {
<div className="relative">
<div className="flex flex-col sm:flex-row items-center gap-1.5">
<span className="group relative normal-case">
<Tooltip
position={'bottom-left'}
additionalClass={
'translate-y-0.5 bg-yellow-300 font-normal !text-black'
}
>
Ask us to feature it once you're done!
</Tooltip>
<button
className="rounded-md bg-black px-3.5 py-1.5 text-sm font-medium text-white transition-colors hover:bg-black"
onClick={() => {
@@ -151,14 +143,6 @@ export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) {
</button>
</span>
<span className="group relative normal-case">
<Tooltip
position={'bottom-left'}
additionalClass={
'translate-y-0.5 bg-yellow-300 font-normal !text-black'
}
>
Up-to-date and maintained by the official team
</Tooltip>
<a
href="/roadmaps"
className="inline-block rounded-md bg-gray-300 px-3.5 py-1.5 text-sm text-black sm:py-1.5 sm:text-sm"
@@ -105,7 +105,7 @@ export function PayToBypass(props: PayToBypassProps) {
id={roadmapCountId}
name={roadmapCountId}
required
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="How many roadmaps you will be generating (daily, or monthly)?"
/>
</div>
@@ -117,7 +117,7 @@ export function PayToBypass(props: PayToBypassProps) {
id={usageId}
name={usageId}
required
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="How will you be using this"
/>
</div>
@@ -131,7 +131,7 @@ export function PayToBypass(props: PayToBypassProps) {
<textarea
id={feedbackId}
name={feedbackId}
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
className="placeholder-text-gray-400 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-sm focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Do you have any feedback?"
/>
</div>
@@ -148,7 +148,7 @@ export function PayToBypass(props: PayToBypassProps) {
</button>
<button
type="submit"
className="disbaled:opacity-60 w-full rounded-lg bg-gray-900 py-2 text-sm text-white hover:bg-gray-800 disabled:cursor-not-allowed"
className="w-full rounded-lg bg-gray-900 py-2 text-sm text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => {
setTimeout(() => {
onClose();
+41
View File
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import type { GetRoadmapResponse } from '../components/CustomRoadmap/CustomRoadmap';
import { httpGet, type FetchError } from '../lib/query-http';
import { queryClient } from '../stores/query-client';
type UseCustomRoadmapOptions = {
slug?: string;
id?: string;
secret?: string;
};
export function useCustomRoadmap(options: UseCustomRoadmapOptions) {
const { slug, id, secret } = options;
return useQuery<GetRoadmapResponse, FetchError>(
{
queryKey: [
'get-roadmap',
{
slug,
id,
},
],
queryFn: async () => {
const roadmapUrl = slug
? new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap-by-slug/${slug}`,
)
: new URL(`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`);
if (secret) {
roadmapUrl.searchParams.set('secret', secret);
}
return httpGet(roadmapUrl.toString());
},
enabled: !!(slug || id),
},
queryClient,
);
}
+146
View File
@@ -0,0 +1,146 @@
import Cookies from 'js-cookie';
import fp from '@fingerprintjs/fingerprintjs';
import { TOKEN_COOKIE_NAME, removeAuthToken } from './jwt.ts';
type HttpOptionsType = RequestInit;
type AppResponse = Record<string, any>;
export interface FetchError extends Error {
status: number;
message: string;
}
type AppError = {
status: number;
message: string;
errors?: { message: string; location: string }[];
};
type ApiReturn<ResponseType> = ResponseType;
/**
* Wrapper around fetch to make it easy to handle errors
*
* @param url
* @param options
*/
export async function httpCall<ResponseType = AppResponse>(
url: string,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
const fullUrl = url.startsWith('http')
? url
: `${import.meta.env.PUBLIC_API_URL}${url}`;
try {
const fingerprintPromise = await fp.load();
const fingerprint = await fingerprintPromise.get();
const isMultiPartFormData = options?.body instanceof FormData;
const headers = new Headers({
Accept: 'application/json',
Authorization: `Bearer ${Cookies.get(TOKEN_COOKIE_NAME)}`,
fp: fingerprint.visitorId,
...(options?.headers ?? {}),
});
if (!isMultiPartFormData) {
headers.set('Content-Type', 'application/json');
}
const response = await fetch(fullUrl, {
credentials: 'include',
...options,
headers,
});
// @ts-ignore
const doesAcceptHtml = options?.headers?.['Accept'] === 'text/html';
const data = doesAcceptHtml ? await response.text() : await response.json();
// Logout user if token is invalid
if (data?.status === 401) {
removeAuthToken();
window.location.href = '/login';
return null as unknown as ApiReturn<ResponseType>;
}
if (!response.ok) {
if (data.errors) {
const error = new Error() as FetchError;
error.message = data.message;
error.status = response?.status;
throw error;
} else {
throw new Error('An unexpected error occurred');
}
}
return data as ResponseType;
} catch (error: any) {
throw error;
}
}
export async function httpPost<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
return httpCall<ResponseType>(url, {
...options,
method: 'POST',
body: body instanceof FormData ? body : JSON.stringify(body),
});
}
export async function httpGet<ResponseType = AppResponse>(
url: string,
queryParams?: Record<string, any>,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
const searchParams = new URLSearchParams(queryParams).toString();
const queryUrl = searchParams ? `${url}?${searchParams}` : url;
return httpCall<ResponseType>(queryUrl, {
credentials: 'include',
method: 'GET',
...options,
});
}
export async function httpPatch<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
return httpCall<ResponseType>(url, {
...options,
method: 'PATCH',
body: JSON.stringify(body),
});
}
export async function httpPut<ResponseType = AppResponse>(
url: string,
body: Record<string, any>,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
return httpCall<ResponseType>(url, {
...options,
method: 'PUT',
body: JSON.stringify(body),
});
}
export async function httpDelete<ResponseType = AppResponse>(
url: string,
options?: HttpOptionsType,
): Promise<ApiReturn<ResponseType>> {
return httpCall<ResponseType>(url, {
...options,
method: 'DELETE',
});
}
+11
View File
@@ -0,0 +1,11 @@
import { QueryCache, QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
queryCache: new QueryCache({}),
defaultOptions: {
queries: {
retry: false,
enabled: !import.meta.env.SSR,
},
},
});