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 =
(typeof allowedCustomRoadmapType)[number];
export const allowedShowcaseStatus = ['visible', 'hidden'] as const;
export type AllowedShowcaseStatus = (typeof allowedShowcaseStatus)[number];
export const allowedRoadmapFeaturedListStatus = [
export const allowedShowcaseStatus = [
'idle',
'submitted',
'approved',
'rejected',
'rejected_with_reason',
] as const;
export type AllowedRoadmapFeaturedListStatus =
(typeof allowedRoadmapFeaturedListStatus)[number];
export type AllowedShowcaseStatus = (typeof allowedShowcaseStatus)[number];
export interface RoadmapDocument {
_id?: string;
_id: string;
title: string;
description?: string;
slug?: string;
@ -61,18 +57,21 @@ export interface RoadmapDocument {
edges: any[];
isDiscoverable?: boolean;
showcaseStatus?: AllowedShowcaseStatus;
ratings: {
average: number;
totalCount: number;
breakdown: {
[key: number]: number;
};
};
featuredListStatus?: AllowedRoadmapFeaturedListStatus;
featuredListRejectedReason?: string;
featuredListSubmittedAt?: Date;
featuredListApprovedAt?: Date;
showcaseStatus?: AllowedShowcaseStatus;
showcaseRejectedReason?: string;
showcaseRejectedAt?: Date;
showcaseSubmittedAt?: Date;
showcaseApprovedAt?: Date;
hasMigratedContent?: boolean;
createdAt: 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 { CustomRoadmapAlert } from './CustomRoadmapAlert.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 = {};
@ -74,126 +75,131 @@ 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}
<>
{$currentRoadmap && <ShowcaseAlert currentRoadmap={$currentRoadmap} />}
<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>
{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="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>
<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,
});
<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?',
);
{$currentRoadmap && (
<FeaturedListingStatus currentRoadmap={$currentRoadmap} />
)}
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);
}}
{((ratings?.average || 0) > 0 ||
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,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 { useToast } from '../../../hooks/use-toast';
type SubmitFeaturedListingWarningProps = {
type SubmitShowcaseWarningProps = {
onClose: () => void;
};
export function SubmitFeaturedListingWarning(
props: SubmitFeaturedListingWarningProps,
export function SubmitShowcaseWarning(
props: SubmitShowcaseWarningProps,
) {
const { onClose } = props;
@ -40,21 +40,21 @@ export function SubmitFeaturedListingWarning(
queryClient,
);
const { featuredListStatus = 'idle', featuredListRejectedReason } =
const { showcaseStatus = 'idle', showcaseRejectedReason } =
$currentRoadmap || {};
return (
<Modal onClose={onClose}>
<div className="p-4">
<h2 className="text-lg font-semibold">
{featuredListStatus === 'rejected_with_reason'
{showcaseStatus === 'rejected_with_reason'
? 'Rejected Reason'
: 'Featured Listing'}
</h2>
<p className="mt-2 text-sm">
{featuredListStatus === 'rejected_with_reason' &&
featuredListRejectedReason}
{featuredListStatus === 'idle' && (
{showcaseStatus === 'rejected_with_reason' &&
showcaseRejectedReason}
{showcaseStatus === 'idle' && (
<>
Submitting your roadmap for a featured listing will make it
visible to everyone on the platform.{' '}
@ -78,7 +78,7 @@ export function SubmitFeaturedListingWarning(
>
{submit.isPending
? 'Submitting...'
: featuredListStatus === 'rejected_with_reason'
: showcaseStatus === 'rejected_with_reason'
? 'Resubmit'
: 'Submit'}
</button>
Loading…
Cancel
Save