diff --git a/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx b/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx index d77a7dbbb..3b6672289 100644 --- a/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx +++ b/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx @@ -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; } diff --git a/src/components/CustomRoadmap/CustomRoadmap.tsx b/src/components/CustomRoadmap/CustomRoadmap.tsx index f28e98545..c9e86684d 100644 --- a/src/components/CustomRoadmap/CustomRoadmap.tsx +++ b/src/components/CustomRoadmap/CustomRoadmap.tsx @@ -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(null); - const [error, setError] = useState(); - 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( - 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; diff --git a/src/components/CustomRoadmap/RoadmapHeader.tsx b/src/components/CustomRoadmap/RoadmapHeader.tsx index 42dcde903..5775d89e3 100644 --- a/src/components/CustomRoadmap/RoadmapHeader.tsx +++ b/src/components/CustomRoadmap/RoadmapHeader.tsx @@ -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 ( -
-
- {!$canManageCurrentRoadmap && } - - {creator?.name && ( -
- {creator.name} - - Created by  - - {creator?.name} - - {team && ( - <> -  from  - - {team?.name} - - - )} - -
+ <> +
+ {$currentRoadmap && $canManageCurrentRoadmap && ( + )} -
-

{title}

-

- {description} -

-
-
-
- - ← -  Discover more - - - +
+ {!$canManageCurrentRoadmap && } + + {creator?.name && ( +
+ {creator.name} + + Created by  + + {creator?.name} + + {team && ( + <> +  from  + + {team?.name} + + + )} + +
+ )} +
+

{title}

+

+ {description} +

-
- {$canManageCurrentRoadmap && ( - <> - {isSharing && $currentRoadmap && ( - setIsSharing(false)} - onShareSettingsUpdate={(settings) => { - currentRoadmap.set({ - ...$currentRoadmap, - ...settings, - }); + +
+ +
+ {$canManageCurrentRoadmap && ( + <> + {isSharing && $currentRoadmap && ( + setIsSharing(false)} + onShareSettingsUpdate={(settings) => { + currentRoadmap.set({ + ...$currentRoadmap, + ...settings, + }); + }} + /> + )} + + {$currentRoadmap && ( + + )} + + 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); }} /> - )} + + )} - 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' && ( + - - )} - - {((ratings?.average || 0) > 0 || showcaseStatus === 'visible') && ( - - )} + )} +
-
- + +
-
+ ); } diff --git a/src/components/CustomRoadmap/Showcase/ShowcaseAlert.tsx b/src/components/CustomRoadmap/Showcase/ShowcaseAlert.tsx new file mode 100644 index 000000000..2f6933cb0 --- /dev/null +++ b/src/components/CustomRoadmap/Showcase/ShowcaseAlert.tsx @@ -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{' '} + + + ), + 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 && ( + { + setShowRejectedReason(false); + }} + /> + )} + +
+
+
+ +
{label}
+
+
+
+ + ); +} diff --git a/src/components/CustomRoadmap/Showcase/ShowcaseStatus.tsx b/src/components/CustomRoadmap/Showcase/ShowcaseStatus.tsx new file mode 100644 index 000000000..cfb84837b --- /dev/null +++ b/src/components/CustomRoadmap/Showcase/ShowcaseStatus.tsx @@ -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 && ( + { + setShowSubmitWarning(false); + }} + /> + )} + + + + ); +} diff --git a/src/components/CustomRoadmap/Showcase/SubmitShowcaseWarning.tsx b/src/components/CustomRoadmap/Showcase/SubmitShowcaseWarning.tsx new file mode 100644 index 000000000..ddf8359af --- /dev/null +++ b/src/components/CustomRoadmap/Showcase/SubmitShowcaseWarning.tsx @@ -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 ( + +
+

+ {showcaseStatus === 'rejected_with_reason' + ? 'Rejected with Reason' + : 'Feature Your Roadmap'} +

+

+ {showcaseStatus === 'rejected_with_reason' && ( + <> + + {showcaseRejectedReason} + + + Feel free to make changes to your roadmap and resubmit. + + + )} + {!showcaseStatus && ( + <> + We will review your roadmap and if accepted, we will make it + public and show it on the community roadmap listing.{' '} + + Are you sure to submit? + + + )} +

+ +
+ + +
+
+
+ ); +} diff --git a/src/components/CustomRoadmap/SkeletonRoadmapHeader.tsx b/src/components/CustomRoadmap/SkeletonRoadmapHeader.tsx index e0d9bd14d..16d7832ef 100644 --- a/src/components/CustomRoadmap/SkeletonRoadmapHeader.tsx +++ b/src/components/CustomRoadmap/SkeletonRoadmapHeader.tsx @@ -2,8 +2,8 @@ export function SkeletonRoadmapHeader() { return (
-
-
+
+
@@ -12,7 +12,7 @@ export function SkeletonRoadmapHeader() {
-
+
diff --git a/src/components/Dashboard/DashboardAiRoadmaps.tsx b/src/components/Dashboard/DashboardAiRoadmaps.tsx index 63539d52d..bcc6ca597 100644 --- a/src/components/Dashboard/DashboardAiRoadmaps.tsx +++ b/src/components/Dashboard/DashboardAiRoadmaps.tsx @@ -27,9 +27,7 @@ export function DashboardAiRoadmaps(props: DashboardAiRoadmapsProps) { return ( <>
-

- My AI Roadmaps -

+

My AI Roadmaps

{roadmaps.map((roadmap) => ( diff --git a/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx b/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx index 7cf290df4..f93b92b92 100644 --- a/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx +++ b/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx @@ -133,14 +133,6 @@ export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) { @@ -131,7 +131,7 @@ export function PayToBypass(props: PayToBypassProps) {