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; done: number;
total: number; total: number;
isCustomResource: boolean; isCustomResource: boolean;
roadmapSlug?: string;
}; };
export type ActivityResponse = { export type ActivityResponse = {
@ -52,7 +53,7 @@ export function ActivityPage() {
async function loadActivity() { async function loadActivity() {
const { error, response } = await httpGet<ActivityResponse>( 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) { if (!response || error) {
@ -107,6 +108,7 @@ export function ActivityPage() {
.map((roadmap) => ( .map((roadmap) => (
<ResourceProgress <ResourceProgress
key={roadmap.id} key={roadmap.id}
roadmapSlug={roadmap.roadmapSlug}
isCustomResource={roadmap.isCustomResource} isCustomResource={roadmap.isCustomResource}
doneCount={roadmap.done || 0} doneCount={roadmap.done || 0}
learningCount={roadmap.learning || 0} learningCount={roadmap.learning || 0}

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

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

@ -82,7 +82,7 @@ export function CreateVersion(props: CreateVersionProps) {
return ( return (
<div className={'flex items-center'}> <div className={'flex items-center'}>
<a <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" 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" /> <Map size="15px" className="mr-1.5" />

@ -27,6 +27,7 @@ export interface RoadmapDocument {
_id?: string; _id?: string;
title: string; title: string;
description?: string; description?: string;
slug?: string;
creatorId: string; creatorId: string;
teamId?: string; teamId?: string;
isDiscoverable: boolean; isDiscoverable: boolean;
@ -145,7 +146,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title" name="title"
id="title" id="title"
required 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" placeholder="Enter Title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
@ -165,8 +166,8 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description" name="description"
required required
className={cn( 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', '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' isInvalidDescription && 'border-red-300 bg-red-100',
)} )}
placeholder="Enter Description" placeholder="Enter Description"
value={description} value={description}

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

@ -18,7 +18,7 @@ import { PersonalRoadmapActionDropdown } from './PersonalRoadmapActionDropdown';
import type { GetRoadmapListResponse } from './RoadmapListPage'; import type { GetRoadmapListResponse } from './RoadmapListPage';
import { useState, type Dispatch, type SetStateAction } from 'react'; import { useState, type Dispatch, type SetStateAction } from 'react';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal'; import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
import {RoadmapIcon} from "../ReactIcons/RoadmapIcon.tsx"; import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
type PersonalRoadmapListType = { type PersonalRoadmapListType = {
roadmaps: GetRoadmapListResponse['personalRoadmaps']; roadmaps: GetRoadmapListResponse['personalRoadmaps'];
@ -37,7 +37,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
async function deleteRoadmap(roadmapId: string) { async function deleteRoadmap(roadmapId: string) {
const { response, error } = await httpDelete<RoadmapDocument[]>( 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) { if (error || !response) {
@ -61,6 +61,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && ( const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal <ShareOptionsModal
roadmapSlug={selectedRoadmap?.slug}
isDiscoverable={selectedRoadmap.isDiscoverable} isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description} description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility} visibility={selectedRoadmap.visibility}
@ -129,7 +130,7 @@ type CustomRoadmapItemProps = {
roadmap: GetRoadmapListResponse['personalRoadmaps'][number]; roadmap: GetRoadmapListResponse['personalRoadmaps'][number];
onRemove: (roadmapId: string) => Promise<void>; onRemove: (roadmapId: string) => Promise<void>;
setSelectedRoadmap: ( setSelectedRoadmap: (
roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null,
) => void; ) => void;
}; };
@ -183,9 +184,9 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
Edit Edit
</a> </a>
<a <a
href={`/r?id=${roadmap._id}`} href={`/r/${roadmap?.slug}`}
className={ 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'} target={'_blank'}
> >

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

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

@ -91,7 +91,7 @@ export function SharedRoadmapList(props: SharedRoadmapListProps) {
className="relative flex w-full border-t" className="relative flex w-full border-t"
> >
<a <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" 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'} target={'_blank'}
> >

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

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

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

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

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

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

@ -473,7 +473,7 @@ export function TeamRoadmaps() {
)} )}
<a <a
href={`/r?id=${resourceConfig.resourceId}`} href={`/r/${resourceConfig.roadmapSlug}`}
className={ 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' '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 BaseLayout from '../../layouts/BaseLayout.astro';
import { CustomRoadmap } from '../../components/CustomRoadmap/CustomRoadmap';
import { SkeletonRoadmapHeader } from '../../components/CustomRoadmap/SkeletonRoadmapHeader'; 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'> <BaseLayout title='Roadmaps'>
<ProgressHelpPopup /> <ProgressHelpPopup />
<div class='mx-auto max-w-2xl p-20'> <div>
<h1>{date}</h1> <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> </div>
</BaseLayout> </BaseLayout>
Loading…
Cancel
Save