feat/featured
Arik Chakma 7 days ago
parent b07a44470b
commit a9755a57ed
  1. 23
      src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx
  2. 72
      src/components/CustomRoadmap/FeaturedListing/FeaturedListingStatus.tsx
  3. 230
      src/components/CustomRoadmap/RoadmapHeader.tsx
  4. 75
      src/components/CustomRoadmap/Showcase/ShowcaseAlert.tsx
  5. 42
      src/components/CustomRoadmap/Showcase/ShowcaseStatus.tsx
  6. 18
      src/components/CustomRoadmap/Showcase/SubmitShowcaseWarning.tsx

@ -23,21 +23,17 @@ export const allowedCustomRoadmapType = ['role', 'skill'] as const;
export type AllowedCustomRoadmapType = export type AllowedCustomRoadmapType =
(typeof allowedCustomRoadmapType)[number]; (typeof allowedCustomRoadmapType)[number];
export const allowedShowcaseStatus = ['visible', 'hidden'] as const; export const allowedShowcaseStatus = [
export type AllowedShowcaseStatus = (typeof allowedShowcaseStatus)[number];
export const allowedRoadmapFeaturedListStatus = [
'idle', 'idle',
'submitted', 'submitted',
'approved', 'approved',
'rejected', 'rejected',
'rejected_with_reason', 'rejected_with_reason',
] as const; ] as const;
export type AllowedRoadmapFeaturedListStatus = export type AllowedShowcaseStatus = (typeof allowedShowcaseStatus)[number];
(typeof allowedRoadmapFeaturedListStatus)[number];
export interface RoadmapDocument { export interface RoadmapDocument {
_id?: string; _id: string;
title: string; title: string;
description?: string; description?: string;
slug?: string; slug?: string;
@ -61,18 +57,21 @@ export interface RoadmapDocument {
edges: any[]; edges: any[];
isDiscoverable?: boolean; isDiscoverable?: boolean;
showcaseStatus?: AllowedShowcaseStatus;
ratings: { ratings: {
average: number; average: number;
totalCount: number;
breakdown: { breakdown: {
[key: number]: number; [key: number]: number;
}; };
}; };
featuredListStatus?: AllowedRoadmapFeaturedListStatus; showcaseStatus?: AllowedShowcaseStatus;
featuredListRejectedReason?: string; showcaseRejectedReason?: string;
featuredListSubmittedAt?: Date; showcaseRejectedAt?: Date;
featuredListApprovedAt?: Date; showcaseSubmittedAt?: Date;
showcaseApprovedAt?: Date;
hasMigratedContent?: boolean;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;

@ -1,72 +0,0 @@
import { useState } from 'react';
import { SubmitFeaturedListingWarning } from './SubmitFeaturedListingWarning';
import type { GetRoadmapResponse } from '../CustomRoadmap';
import { CheckIcon, EyeIcon, FlagIcon, SendIcon, XIcon } from 'lucide-react';
import { cn } from '../../../lib/classname';
type FeaturedListingStatusProps = {
currentRoadmap: GetRoadmapResponse;
};
export function FeaturedListingStatus(props: FeaturedListingStatusProps) {
const { currentRoadmap } = props;
const { featuredListStatus = 'idle' } = currentRoadmap;
const [showSubmitWarning, setShowSubmitWarning] = useState(false);
const currentLabel = {
idle: {
icon: SendIcon,
label: 'Submit for Featured Listing',
className: 'bg-gray-100 text-gray-600 border-gray-200',
},
submitted: {
icon: EyeIcon,
label: 'Waiting for Approval',
className: 'bg-blue-100 text-blue-600 border-blue-200',
},
approved: {
icon: CheckIcon,
label: 'Approved',
className: 'bg-green-100 text-green-600 border-green-200',
},
rejected: {
icon: XIcon,
label: 'Rejected',
className: 'bg-red-100 text-red-600 border-red-200',
},
rejected_with_reason: {
icon: FlagIcon,
label: 'Changes Requested',
className: 'bg-yellow-100 text-yellow-600 border-yellow-200',
},
}[featuredListStatus];
return (
<>
{showSubmitWarning && (
<SubmitFeaturedListingWarning
onClose={() => {
setShowSubmitWarning(false);
}}
/>
)}
<button
className={cn(
'flex items-center gap-1.5 rounded-full border px-2 text-sm',
currentLabel?.className,
)}
onClick={() => {
setShowSubmitWarning(true);
}}
disabled={
!['idle', 'rejected_with_reason'].includes(featuredListStatus)
}
>
<currentLabel.icon className="size-3 stroke-[2.5]" />
{currentLabel.label}
</button>
</>
);
}

@ -11,7 +11,8 @@ import { RoadmapActionButton } from './RoadmapActionButton';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
import { CustomRoadmapAlert } from './CustomRoadmapAlert.tsx'; import { CustomRoadmapAlert } from './CustomRoadmapAlert.tsx';
import { CustomRoadmapRatings } from './CustomRoadmapRatings.tsx'; import { CustomRoadmapRatings } from './CustomRoadmapRatings.tsx';
import { FeaturedListingStatus } from './FeaturedListing/FeaturedListingStatus.tsx'; import { ShowcaseStatus } from './Showcase/ShowcaseStatus.tsx';
import { ShowcaseAlert } from './Showcase/ShowcaseAlert.tsx';
type RoadmapHeaderProps = {}; type RoadmapHeaderProps = {};
@ -74,126 +75,131 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
: '/images/default-avatar.png'; : '/images/default-avatar.png';
return ( return (
<div className="border-b"> <>
<div className="container relative py-5 sm:py-12"> {$currentRoadmap && <ShowcaseAlert currentRoadmap={$currentRoadmap} />}
{!$canManageCurrentRoadmap && <CustomRoadmapAlert />}
<div className="border-b">
{creator?.name && ( <div className="container relative py-5 sm:py-12">
<div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500"> {!$canManageCurrentRoadmap && <CustomRoadmapAlert />}
<img
alt={creator.name} {creator?.name && (
src={avatarUrl} <div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500">
className="h-5 w-5 rounded-full" <img
/> alt={creator.name}
<span> src={avatarUrl}
Created by&nbsp; className="h-5 w-5 rounded-full"
<span className="font-semibold text-gray-900"> />
{creator?.name} <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> </span>
{team && ( </div>
<> )}
&nbsp;from&nbsp; <div className="mb-3 mt-4 sm:mb-4">
<span className="font-semibold text-gray-900"> <h1 className="text-2xl font-bold sm:mb-2 sm:text-4xl">{title}</h1>
{team?.name} <p className="mt-0.5 text-sm text-gray-500 sm:text-lg">
</span> {description}
</> </p>
)}
</span>
</div> </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 justify-between gap-2 sm:gap-0"> <div className="flex justify-between gap-2 sm:gap-0">
<div className="flex justify-stretch gap-1 sm:gap-2"> <div className="flex justify-stretch gap-1 sm:gap-2">
<a <a
href="/community" 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" 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" aria-label="Back to All Roadmaps"
> >
&larr; &larr;
<span className="hidden sm:inline">&nbsp;Discover more</span> <span className="hidden sm:inline">&nbsp;Discover more</span>
</a> </a>
<ShareRoadmapButton <ShareRoadmapButton
roadmapId={roadmapId!} roadmapId={roadmapId!}
description={description!} description={description!}
pageUrl={`https://roadmap.sh/r/${roadmapSlug}`} pageUrl={`https://roadmap.sh/r/${roadmapSlug}`}
allowEmbed={true} allowEmbed={true}
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{$canManageCurrentRoadmap && ( {$canManageCurrentRoadmap && (
<> <>
{isSharing && $currentRoadmap && ( {isSharing && $currentRoadmap && (
<ShareOptionsModal <ShareOptionsModal
roadmapSlug={$currentRoadmap?.slug} roadmapSlug={$currentRoadmap?.slug}
isDiscoverable={$currentRoadmap.isDiscoverable} isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description} description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility} visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId} teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!} roadmapId={$currentRoadmap?._id!}
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []} sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
sharedTeamMemberIds={ sharedTeamMemberIds={
$currentRoadmap?.sharedTeamMemberIds || [] $currentRoadmap?.sharedTeamMemberIds || []
} }
onClose={() => setIsSharing(false)} onClose={() => setIsSharing(false)}
onShareSettingsUpdate={(settings) => { onShareSettingsUpdate={(settings) => {
currentRoadmap.set({ currentRoadmap.set({
...$currentRoadmap, ...$currentRoadmap,
...settings, ...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?',
);
{$currentRoadmap && ( if (!confirmation) {
<FeaturedListingStatus currentRoadmap={$currentRoadmap} /> return;
)} }
deleteResource().finally(() => null);
}}
/>
</>
)}
<RoadmapActionButton {((ratings?.average || 0) > 0 ||
onUpdateSharing={() => setIsSharing(true)} showcaseStatus === 'approved') && (
onCustomize={() => { <CustomRoadmapRatings
window.location.href = `${ roadmapSlug={roadmapSlug!}
import.meta.env.PUBLIC_EDITOR_APP_URL ratings={ratings!}
}/${$currentRoadmap?._id}`; canManage={$canManageCurrentRoadmap}
}} unseenRatingCount={unseenRatingCount || 0}
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?',
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
/> />
</> )}
)} </div>
{((ratings?.average || 0) > 0 || showcaseStatus === 'visible') && (
<CustomRoadmapRatings
roadmapSlug={roadmapSlug!}
ratings={ratings!}
canManage={$canManageCurrentRoadmap}
unseenRatingCount={unseenRatingCount || 0}
/>
)}
</div> </div>
</div>
<RoadmapHint <RoadmapHint
roadmapTitle={title!} roadmapTitle={title!}
hasTNSBanner={false} hasTNSBanner={false}
roadmapId={roadmapId!} roadmapId={roadmapId!}
/> />
</div>
</div> </div>
</div> </>
); );
} }

@ -0,0 +1,75 @@
import {
CheckIcon,
EyeIcon,
FlagIcon,
FrownIcon,
SmileIcon,
XIcon,
} from 'lucide-react';
import { cn } from '../../../lib/classname';
import type { GetRoadmapResponse } from '../CustomRoadmap';
type ShowcaseAlertProps = {
currentRoadmap: GetRoadmapResponse;
};
export function ShowcaseAlert(props: ShowcaseAlertProps) {
const { currentRoadmap } = props;
// const { showcaseStatus = 'idle' } = currentRoadmap;
// if (showcaseStatus === 'idle') {
// return null;
// }
const showcaseStatus = 'rejected_with_reason';
const showcaseStatusMap = {
submitted: {
icon: EyeIcon,
label:
'We are reviewing your roadmap. It will be visible to everyone on the platform once approved.',
className: 'text-blue-600 border-blue-200',
},
approved: {
icon: SmileIcon,
label: 'Hooray! Your roadmap is now visible to everyone on the platform.',
className: 'text-green-600 border-green-200',
},
rejected: {
icon: FrownIcon,
label: 'Sorry, we are unable to feature your roadmap at this time.',
className: 'text-red-600 border-red-200',
},
rejected_with_reason: {
icon: FlagIcon,
label: (
<>
Your roadmap needs changes before it can be featured.{' '}
<button className="font-medium underline underline-offset-2 hover:no-underline">
Check Reason
</button>
</>
),
className: 'text-yellow-600 border-yellow-200',
},
};
const { icon: Icon, label, className } = showcaseStatusMap[showcaseStatus];
return (
<div
className={cn(
showcaseStatus === 'submitted' && 'bg-blue-100',
showcaseStatus === 'approved' && 'bg-green-100',
showcaseStatus === 'rejected' && 'bg-red-100',
showcaseStatus === 'rejected_with_reason' && 'bg-yellow-100',
)}
>
<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 = 'idle' } = currentRoadmap;
const [showSubmitWarning, setShowSubmitWarning] = useState(false);
if (!currentRoadmap || showcaseStatus !== 'idle') {
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 !== 'idle'}
>
<SendIcon className="mr-0 h-4 w-4 stroke-[2.5] sm:mr-1.5" />
<span className="hidden sm:inline">Submit for Showcase</span>
</button>
</>
);
}

@ -6,12 +6,12 @@ import { useStore } from '@nanostores/react';
import { currentRoadmap } from '../../../stores/roadmap'; import { currentRoadmap } from '../../../stores/roadmap';
import { useToast } from '../../../hooks/use-toast'; import { useToast } from '../../../hooks/use-toast';
type SubmitFeaturedListingWarningProps = { type SubmitShowcaseWarningProps = {
onClose: () => void; onClose: () => void;
}; };
export function SubmitFeaturedListingWarning( export function SubmitShowcaseWarning(
props: SubmitFeaturedListingWarningProps, props: SubmitShowcaseWarningProps,
) { ) {
const { onClose } = props; const { onClose } = props;
@ -40,21 +40,21 @@ export function SubmitFeaturedListingWarning(
queryClient, queryClient,
); );
const { featuredListStatus = 'idle', featuredListRejectedReason } = const { showcaseStatus = 'idle', showcaseRejectedReason } =
$currentRoadmap || {}; $currentRoadmap || {};
return ( return (
<Modal onClose={onClose}> <Modal onClose={onClose}>
<div className="p-4"> <div className="p-4">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{featuredListStatus === 'rejected_with_reason' {showcaseStatus === 'rejected_with_reason'
? 'Rejected Reason' ? 'Rejected Reason'
: 'Featured Listing'} : 'Featured Listing'}
</h2> </h2>
<p className="mt-2 text-sm"> <p className="mt-2 text-sm">
{featuredListStatus === 'rejected_with_reason' && {showcaseStatus === 'rejected_with_reason' &&
featuredListRejectedReason} showcaseRejectedReason}
{featuredListStatus === 'idle' && ( {showcaseStatus === 'idle' && (
<> <>
Submitting your roadmap for a featured listing will make it Submitting your roadmap for a featured listing will make it
visible to everyone on the platform.{' '} visible to everyone on the platform.{' '}
@ -78,7 +78,7 @@ export function SubmitFeaturedListingWarning(
> >
{submit.isPending {submit.isPending
? 'Submitting...' ? 'Submitting...'
: featuredListStatus === 'rejected_with_reason' : showcaseStatus === 'rejected_with_reason'
? 'Resubmit' ? 'Resubmit'
: 'Submit'} : 'Submit'}
</button> </button>
Loading…
Cancel
Save