feat: custom roadmap slug routes (#4987)

* feat: replace roadmap slug

* fix: remove roadmap slug
pull/5494/head
Arik Chakma 10 months ago committed by GitHub
parent febeb6f586
commit 13c55faa71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      src/components/Activity/ActivityPage.tsx
  2. 6
      src/components/Activity/ResourceProgress.tsx
  3. 9
      src/components/CreateTeam/RoadmapSelector.tsx
  4. 2
      src/components/CreateVersion/CreateVersion.tsx
  5. 7
      src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx
  6. 24
      src/components/CustomRoadmap/CustomRoadmap.tsx
  7. 11
      src/components/CustomRoadmap/PersonalRoadmapList.tsx
  8. 3
      src/components/CustomRoadmap/ResourceProgressStats.tsx
  9. 5
      src/components/CustomRoadmap/RoadmapHeader.tsx
  10. 2
      src/components/CustomRoadmap/SharedRoadmapList.tsx
  11. 7
      src/components/HeroSection/FavoriteRoadmaps.tsx
  12. 6
      src/components/HeroSection/HeroRoadmaps.tsx
  13. 29
      src/components/ShareOptions/ShareOptionsModal.tsx
  14. 24
      src/components/ShareOptions/ShareSuccess.tsx
  15. 4
      src/components/TeamProgress/GroupRoadmapItem.tsx
  16. 11
      src/components/TeamProgress/TeamProgressPage.tsx
  17. 2
      src/components/TeamRoadmapsList/TeamRoadmaps.tsx
  18. 20
      src/pages/r/[customRoadmapSlug].astro

@ -14,6 +14,7 @@ type ProgressResponse = {
done: number;
total: number;
isCustomResource: boolean;
roadmapSlug?: string;
};
export type ActivityResponse = {
@ -52,7 +53,7 @@ export function ActivityPage() {
async function loadActivity() {
const { error, response } = await httpGet<ActivityResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-stats`,
);
if (!response || error) {
@ -107,6 +108,7 @@ export function ActivityPage() {
.map((roadmap) => (
<ResourceProgress
key={roadmap.id}
roadmapSlug={roadmap.roadmapSlug}
isCustomResource={roadmap.isCustomResource}
doneCount={roadmap.done || 0}
learningCount={roadmap.learning || 0}

@ -17,6 +17,7 @@ type ResourceProgressType = {
onCleared?: () => void;
showClearButton?: boolean;
isCustomResource: boolean;
roadmapSlug?: string;
};
export function ResourceProgress(props: ResourceProgressType) {
@ -37,6 +38,7 @@ export function ResourceProgress(props: ResourceProgressType) {
doneCount,
skippedCount,
onCleared,
roadmapSlug,
} = props;
async function clearProgress() {
@ -46,7 +48,7 @@ export function ResourceProgress(props: ResourceProgressType) {
{
resourceId,
resourceType,
}
},
);
if (error || !response) {
@ -72,7 +74,7 @@ export function ResourceProgress(props: ResourceProgressType) {
: `/best-practices/${resourceId}`;
if (isCustomResource) {
url = `/r?id=${resourceId}`;
url = `/r/${roadmapSlug}`;
}
const totalMarked = doneCount + skippedCount;

@ -14,6 +14,7 @@ import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
roadmapSlug?: string;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
@ -80,7 +81,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
{
resourceId: roadmapId,
resourceType: 'roadmap',
}
},
);
if (error || !response) {
@ -114,7 +115,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
resourceId: roadmapId,
resourceType: 'roadmap',
removed: [],
}
},
);
if (error || !response) {
@ -312,7 +313,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
`${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${resourceId}`,
'_blank'
'_blank',
);
return;
}
@ -335,7 +336,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
)}
</div>
);
}
},
)}
</div>
)}

@ -82,7 +82,7 @@ export function CreateVersion(props: CreateVersionProps) {
return (
<div className={'flex items-center'}>
<a
href={`/r?id=${userVersion._id}`}
href={`/r/${userVersion?.slug}`}
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" />

@ -27,6 +27,7 @@ export interface RoadmapDocument {
_id?: string;
title: string;
description?: string;
slug?: string;
creatorId: string;
teamId?: string;
isDiscoverable: boolean;
@ -145,7 +146,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title"
id="title"
required
className="block text-black w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm"
placeholder="Enter Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
@ -165,8 +166,8 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description"
required
className={cn(
'block text-black h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
isInvalidDescription && 'border-red-300 bg-red-100'
'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm',
isInvalidDescription && 'border-red-300 bg-red-100',
)}
placeholder="Enter Description"
value={description}

@ -62,10 +62,11 @@ export function hideRoadmapLoader() {
type CustomRoadmapProps = {
isEmbed?: boolean;
slug?: string;
};
export function CustomRoadmap(props: CustomRoadmapProps) {
const { isEmbed = false } = props;
const { isEmbed = false, slug } = props;
const { id, secret } = getUrlParams() as { id: string; secret: string };
@ -76,9 +77,11 @@ export function CustomRoadmap(props: CustomRoadmapProps) {
async function getRoadmap() {
setIsLoading(true);
const roadmapUrl = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`,
);
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);
@ -102,12 +105,12 @@ export function CustomRoadmap(props: CustomRoadmapProps) {
}
async function trackVisit() {
if (!isLoggedIn() || isEmbed) {
if (!isLoggedIn() || isEmbed || !roadmap) {
return;
}
await httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
resourceId: id,
resourceId: roadmap?._id,
resourceType: 'roadmap',
});
}
@ -116,9 +119,16 @@ export function CustomRoadmap(props: CustomRoadmapProps) {
getRoadmap().finally(() => {
hideRoadmapLoader();
});
trackVisit().then();
}, []);
useEffect(() => {
if (!roadmap) {
return;
}
trackVisit().then();
}, [roadmap]);
if (isLoading) {
return null;
}

@ -18,7 +18,7 @@ import { PersonalRoadmapActionDropdown } from './PersonalRoadmapActionDropdown';
import type { GetRoadmapListResponse } from './RoadmapListPage';
import { useState, type Dispatch, type SetStateAction } from 'react';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
import {RoadmapIcon} from "../ReactIcons/RoadmapIcon.tsx";
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
type PersonalRoadmapListType = {
roadmaps: GetRoadmapListResponse['personalRoadmaps'];
@ -37,7 +37,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
async function deleteRoadmap(roadmapId: string) {
const { response, error } = await httpDelete<RoadmapDocument[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-roadmap/${roadmapId}`
`${import.meta.env.PUBLIC_API_URL}/v1-delete-roadmap/${roadmapId}`,
);
if (error || !response) {
@ -61,6 +61,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
roadmapSlug={selectedRoadmap?.slug}
isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
@ -129,7 +130,7 @@ type CustomRoadmapItemProps = {
roadmap: GetRoadmapListResponse['personalRoadmaps'][number];
onRemove: (roadmapId: string) => Promise<void>;
setSelectedRoadmap: (
roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null
roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null,
) => void;
};
@ -183,9 +184,9 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
Edit
</a>
<a
href={`/r?id=${roadmap._id}`}
href={`/r/${roadmap?.slug}`}
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'
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs text-blue-600 hover:bg-blue-50 focus:outline-none'
}
target={'_blank'}
>

@ -24,6 +24,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
roadmapSlug={$currentRoadmap?.slug}
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
@ -47,7 +48,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
{
'rounded-bl-md rounded-br-md': isSecondaryBanner,
'rounded-md': !isSecondaryBanner,
}
},
)}
>
<p

@ -23,6 +23,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
title,
description,
_id: roadmapId,
slug: roadmapSlug,
creator,
team,
visibility,
@ -78,6 +79,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
>
<ShareSuccess
visibility="public"
roadmapSlug={roadmapSlug}
roadmapId={roadmapId!}
description={description}
onClose={() => setIsSharingWithOthers(false)}
@ -132,7 +134,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
<ShareRoadmapButton
roadmapId={roadmapId!}
description={description!}
pageUrl={`https://roadmap.sh/r?id=${roadmapId}`}
pageUrl={`https://roadmap.sh/r/${roadmapSlug}`}
allowEmbed={true}
/>
</div>
@ -141,6 +143,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
<>
{isSharing && $currentRoadmap && (
<ShareOptionsModal
roadmapSlug={$currentRoadmap?.slug}
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}

@ -91,7 +91,7 @@ export function SharedRoadmapList(props: SharedRoadmapListProps) {
className="relative flex w-full border-t"
>
<a
href={`/r?id=${roadmap._id}`}
href={`/r/=${roadmap?.slug}`}
className="group inline-grid w-full grid-cols-[auto,16px] items-center justify-between gap-2 px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-black"
target={'_blank'}
>

@ -16,6 +16,7 @@ export type UserProgressResponse = {
total: number;
updatedAt: Date;
isCustomResource: boolean;
roadmapSlug?: string;
team?: {
name: string;
id: string;
@ -41,7 +42,7 @@ function renderProgress(progressList: UserProgressResponse) {
resourceType: progress.resourceType,
isFavorite: progress.isFavorite,
},
})
}),
);
const totalDone = progress.done + progress.skipped;
@ -89,7 +90,7 @@ export function FavoriteRoadmaps() {
setIsLoading(true);
const { response: progressList, error } = await httpGet<ProgressResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
);
if (error || !progressList) {
@ -121,7 +122,7 @@ export function FavoriteRoadmaps() {
const hasProgress = progress?.length > 0;
const customRoadmaps = progress?.filter(
(p) => p.isCustomResource && !p.team?.name
(p) => p.isCustomResource && !p.team?.name,
);
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
const teamRoadmaps: HeroTeamRoadmaps = progress

@ -172,7 +172,7 @@ export function HeroRoadmaps(props: ProgressListProps) {
customRoadmap.total) *
100
}
url={`/r?id=${customRoadmap.resourceId}`}
url={`/r/${customRoadmap?.roadmapSlug}`}
allowFavorite={false}
/>
);
@ -187,7 +187,7 @@ export function HeroRoadmaps(props: ProgressListProps) {
const currentTeam: UserProgressResponse[0]['team'] =
teamRoadmaps?.[teamName]?.[0]?.team;
const roadmapsList = teamRoadmaps[teamName].filter(
(roadmap) => !!roadmap.resourceTitle
(roadmap) => !!roadmap.resourceTitle,
);
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
@ -242,7 +242,7 @@ export function HeroRoadmaps(props: ProgressListProps) {
customRoadmap.total) *
100
}
url={`/r?id=${customRoadmap.resourceId}`}
url={`/r/${customRoadmap?.roadmapSlug}`}
allowFavorite={false}
/>
);

@ -1,10 +1,4 @@
import {
type ReactNode,
useCallback,
useState,
useMemo,
useEffect,
} from 'react';
import { type ReactNode, useCallback, useState, useMemo } from 'react';
import { Globe2, Loader2, Lock } from 'lucide-react';
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
import { TransferToTeamList } from './TransferToTeamList';
@ -37,6 +31,7 @@ type ShareOptionsModalProps = {
teamId?: string;
roadmapId?: string;
description?: string;
roadmapSlug?: string;
onShareSettingsUpdate: OnShareSettingsUpdate;
};
@ -44,6 +39,7 @@ type ShareOptionsModalProps = {
export function ShareOptionsModal(props: ShareOptionsModalProps) {
const {
roadmapId,
roadmapSlug,
onClose,
isDiscoverable: defaultIsDiscoverable = false,
visibility: defaultVisibility,
@ -68,10 +64,10 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
const [visibility, setVisibility] = useState(defaultVisibility);
const [isDiscoverable, setIsDiscoverable] = useState(defaultIsDiscoverable);
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
defaultSharedMemberIds
defaultSharedMemberIds,
);
const [sharedFriendIds, setSharedFriendIds] = useState<string[]>(
defaultSharedFriendIds
defaultSharedFriendIds,
);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
@ -120,7 +116,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
sharedFriendIds,
sharedTeamMemberIds,
isDiscoverable,
}
},
);
if (error) {
@ -151,7 +147,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
teamId,
sharedTeamMemberIds,
isDiscoverable,
}
},
);
if (error) {
@ -162,7 +158,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
window.location.reload();
},
[roadmapId]
[roadmapId],
);
if (isSettingsUpdated) {
@ -173,6 +169,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
bodyClassName="p-4 flex flex-col"
>
<ShareSuccess
roadmapSlug={roadmapSlug}
visibility={visibility}
roadmapId={roadmapId!}
description={description}
@ -212,11 +209,11 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
setSharedFriendIds([]);
} else if (visibility === 'friends') {
setSharedFriendIds(
defaultSharedFriendIds.length > 0 ? defaultSharedFriendIds : []
defaultSharedFriendIds.length > 0 ? defaultSharedFriendIds : [],
);
} else if (visibility === 'team' && teamId) {
setSharedTeamMemberIds(
defaultSharedMemberIds?.length > 0 ? defaultSharedMemberIds : []
defaultSharedMemberIds?.length > 0 ? defaultSharedMemberIds : [],
);
setSharedFriendIds([]);
} else {
@ -329,7 +326,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
}
onClick={() => {
handleTransferToTeam(selectedTeamId!, sharedTeamMemberIds).then(
() => null
() => null,
);
}}
>
@ -374,7 +371,7 @@ function UpdateAction(props: {
className={cn(
'flex min-w-[120px] items-center justify-center gap-1.5 rounded-md border border-gray-900 bg-gray-900 px-4 py-2 text-white hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-75',
disabled && 'border-gray-700 bg-gray-700 text-white hover:bg-gray-700',
className
className,
)}
disabled={disabled}
onClick={onClick}

@ -4,6 +4,7 @@ import { cn } from '../../lib/classname';
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
type ShareSuccessProps = {
roadmapSlug?: string;
roadmapId: string;
onClose: () => void;
visibility: AllowedRoadmapVisibility;
@ -13,6 +14,7 @@ type ShareSuccessProps = {
export function ShareSuccess(props: ShareSuccessProps) {
const {
roadmapSlug,
roadmapId,
onClose,
description,
@ -23,7 +25,9 @@ export function ShareSuccess(props: ShareSuccessProps) {
const baseUrl = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://roadmap.sh';
const shareLink = `${baseUrl}/r?id=${roadmapId}`;
const shareLink = roadmapSlug
? `${baseUrl}/r/${roadmapSlug}`
: `${baseUrl}/r?id=${roadmapId}`;
const { copyText, isCopied } = useCopyText();
@ -84,13 +88,13 @@ export function ShareSuccess(props: ShareSuccessProps) {
</p>
<div className="mt-2">
<input
onClick={(e) => {
e.currentTarget.select();
copyText(embedHtml);
}}
readOnly={true}
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
value={embedHtml}
onClick={(e) => {
e.currentTarget.select();
copyText(embedHtml);
}}
readOnly={true}
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
value={embedHtml}
/>
</div>
</div>
@ -127,7 +131,7 @@ export function ShareSuccess(props: ShareSuccessProps) {
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded bg-black px-4 py-2.5 text-sm font-medium text-white hover:opacity-80',
isCopied && 'bg-green-300 text-green-800'
isCopied && 'bg-green-300 text-green-800',
)}
disabled={isCopied}
onClick={() => {
@ -139,7 +143,7 @@ export function ShareSuccess(props: ShareSuccessProps) {
</button>
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100'
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100',
)}
onClick={onClose}
>

@ -11,7 +11,7 @@ type GroupRoadmapItemProps = {
export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
const { onShowResourceProgress } = props;
const { members, resourceTitle, resourceId, isCustomResource } =
const { members, resourceTitle, resourceId, isCustomResource, roadmapSlug } =
props.roadmap;
const { t: teamId } = getUrlParams();
@ -19,7 +19,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
const [showAll, setShowAll] = useState(false);
const roadmapLink = isCustomResource
? `/r?id=${resourceId}`
? `/r/${roadmapSlug}`
: `/${resourceId}?t=${teamId}`;
return (

@ -22,6 +22,7 @@ export type UserProgress = {
total: number;
updatedAt: string;
isCustomResource?: boolean;
roadmapSlug?: string;
};
export type TeamMember = {
@ -39,6 +40,7 @@ export type GroupByRoadmap = {
resourceTitle: string;
resourceType: string;
isCustomResource?: boolean;
roadmapSlug?: string;
members: {
member: TeamMember;
progress: UserProgress | undefined;
@ -71,7 +73,7 @@ export function TeamProgressPage() {
async function getTeamProgress() {
const { response, error } = await httpGet<TeamMember[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-progress/${teamId}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-progress/${teamId}`,
);
if (error || !response) {
toast.error(error?.message || 'Failed to get team progress');
@ -87,7 +89,7 @@ export function TeamProgressPage() {
return 1;
}
return 0;
})
}),
);
}
@ -116,7 +118,7 @@ export function TeamProgressPage() {
const members: GroupByRoadmap['members'] = [];
for (const member of teamMembers) {
const progress = member.progress.find(
(progress) => progress.resourceId === roadmap
(progress) => progress.resourceId === roadmap,
);
if (!progress) {
continue;
@ -139,6 +141,7 @@ export function TeamProgressPage() {
resourceId: roadmap,
resourceTitle: members?.[0].progress?.resourceTitle || '',
resourceType: 'roadmap',
roadmapSlug: members?.[0].progress?.roadmapSlug,
members,
isCustomResource,
});
@ -174,7 +177,7 @@ export function TeamProgressPage() {
setShowMemberProgress({
resourceId: showMemberProgress.resourceId,
member: teamMembers.find(
(member) => member.email === user?.email
(member) => member.email === user?.email,
)!,
isCustomResource: showMemberProgress.isCustomResource,
});

@ -473,7 +473,7 @@ export function TeamRoadmaps() {
)}
<a
href={`/r?id=${resourceConfig.resourceId}`}
href={`/r/${resourceConfig.roadmapSlug}`}
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'
}

@ -1,16 +1,24 @@
---
import ProgressHelpPopup from '../../components/ProgressHelpPopup.astro';
import Loader from '../../components/Loader.astro';
import { CustomRoadmap } from '../../components/CustomRoadmap/CustomRoadmap';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { CustomRoadmap } from '../../components/CustomRoadmap/CustomRoadmap';
import { SkeletonRoadmapHeader } from '../../components/CustomRoadmap/SkeletonRoadmapHeader';
import Loader from '../../components/Loader.astro';
import ProgressHelpPopup from '../../components/ProgressHelpPopup.astro';
const date = new Date();
const { customRoadmapSlug } = Astro.params;
---
<BaseLayout title='Roadmaps'>
<ProgressHelpPopup />
<div class='mx-auto max-w-2xl p-20'>
<h1>{date}</h1>
<div>
<div class='flex min-h-[550px] flex-col'>
<div data-roadmap-loader class='flex w-full grow flex-col'>
<SkeletonRoadmapHeader />
<div class='flex grow items-center justify-center'>
<Loader />
</div>
</div>
<CustomRoadmap slug={customRoadmapSlug} client:only='react' />
</div>
</div>
</BaseLayout>
Loading…
Cancel
Save