-
+
+
+
+ Project Ideas
+
+
+
+ {projectGroups.map((projectGroup) => {
+ return (
+
+ );
+ })}
+
- )}
+
- {imgUrl && (
-
-

+
+
+
+ Best Practices
+
+
+
+ {builtInBestPractices.map((roadmap) => {
+ const roadmapProgress = learningRoadmapsToShow.find(
+ (lr) => lr.resourceId === roadmap.id,
+ );
+
+ const percentageDone =
+ (((roadmapProgress?.skipped || 0) +
+ (roadmapProgress?.done || 0)) /
+ (roadmapProgress?.total || 1)) *
+ 100;
+
+ return (
+
+ );
+ })}
+
- )}
+
-
-
{title}
-
{description}
+
+
+
+ Questions
+
+
+
+ {questionGroups.map((questionGroup) => {
+ return (
+
+ );
+ })}
+
+
-
+
- {externalLinkHref && (
-
- {ExternalLinkIcon && }
- {externalLinkText}
-
- )}
+
+ questionGroup.frontmatter.authorId)
+ .slice(0, 7)}
+ />
+
+
);
}
-
-function DashboardCardSkeleton() {
- return (
-
- );
-}
diff --git a/src/components/FeaturedGuides.astro b/src/components/FeaturedGuides.astro
deleted file mode 100644
index 3d78a90b8..000000000
--- a/src/components/FeaturedGuides.astro
+++ /dev/null
@@ -1,47 +0,0 @@
----
-import type { GuideFileType } from '../lib/guide';
-import GuideListItem from './GuideListItem.astro';
-import type { QuestionGroupType } from '../lib/question-group';
-
-export interface Props {
- heading: string;
- guides: GuideFileType[];
- questions: QuestionGroupType[];
-}
-
-const { heading, guides, questions = [] } = Astro.props;
-
-const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
- ...guides,
- ...questions,
-].sort((a, b) => {
- const aDate = new Date(a.frontmatter.date as string);
- const bDate = new Date(b.frontmatter.date as string);
-
- return bDate.getTime() - aDate.getTime();
-});
----
-
-
diff --git a/src/components/FeaturedGuides/FeaturedGuideList.tsx b/src/components/FeaturedGuides/FeaturedGuideList.tsx
new file mode 100644
index 000000000..15427f2d9
--- /dev/null
+++ b/src/components/FeaturedGuides/FeaturedGuideList.tsx
@@ -0,0 +1,51 @@
+import type { GuideFileType } from '../../lib/guide';
+import type { QuestionGroupType } from '../../lib/question-group';
+import { GuideListItem } from './GuideListItem';
+
+export interface FeaturedGuidesProps {
+ heading: string;
+ guides: GuideFileType[];
+ questions: QuestionGroupType[];
+}
+
+export function FeaturedGuideList(props: FeaturedGuidesProps) {
+ const { heading, guides, questions = [] } = props;
+
+ const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
+ ...guides,
+ ...questions,
+ ].sort((a, b) => {
+ const aDate = new Date(a.frontmatter.date as string);
+ const bDate = new Date(b.frontmatter.date as string);
+
+ return bDate.getTime() - aDate.getTime();
+ });
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/FeaturedGuides/GuideListItem.tsx b/src/components/FeaturedGuides/GuideListItem.tsx
new file mode 100644
index 000000000..70cafebce
--- /dev/null
+++ b/src/components/FeaturedGuides/GuideListItem.tsx
@@ -0,0 +1,57 @@
+import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
+import { type QuestionGroupType } from '../../lib/question-group';
+
+export interface GuideListItemProps {
+ guide: GuideFileType | QuestionGroupType;
+}
+
+function isQuestionGroupType(
+ guide: GuideFileType | QuestionGroupType,
+): guide is QuestionGroupType {
+ return (guide as QuestionGroupType).questions !== undefined;
+}
+
+export function GuideListItem(props: GuideListItemProps) {
+ const { guide } = props;
+ const { frontmatter, id } = guide;
+
+ let pageUrl = '';
+ let guideType = '';
+
+ if (isQuestionGroupType(guide)) {
+ pageUrl = `/questions/${id}`;
+ guideType = 'Questions';
+ } else {
+ const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
+ pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
+ guideType = (frontmatter as GuideFrontmatter).type;
+ }
+
+ return (
+
+
+ {frontmatter.title}
+
+ {frontmatter.isNew && (
+
+ New
+
+ ·
+ {new Date(frontmatter.date || '').toLocaleString('default', {
+ month: 'long',
+ })}
+
+
+ )}
+
+
+ {guideType}
+
+
+ »
+
+ );
+}
diff --git a/src/components/FeaturedVideos.astro b/src/components/FeaturedVideos.astro
deleted file mode 100644
index dbd70b792..000000000
--- a/src/components/FeaturedVideos.astro
+++ /dev/null
@@ -1,35 +0,0 @@
----
-import type { VideoFileType } from '../lib/video';
-import VideoListItem from './VideoListItem.astro';
-
-export interface Props {
- heading: string;
- videos: VideoFileType[];
-}
-
-const { heading, videos } = Astro.props;
----
-
-
\ No newline at end of file
diff --git a/src/components/FeaturedVideos/FeaturedVideoList.tsx b/src/components/FeaturedVideos/FeaturedVideoList.tsx
new file mode 100644
index 000000000..4001c8241
--- /dev/null
+++ b/src/components/FeaturedVideos/FeaturedVideoList.tsx
@@ -0,0 +1,39 @@
+import type { VideoFileType } from '../../lib/video';
+import { VideoListItem } from './VideoListItem';
+
+export interface FeaturedVideoListProps {
+ heading: string;
+ videos: VideoFileType[];
+}
+
+export function FeaturedVideoList(props: FeaturedVideoListProps) {
+ const { heading, videos } = props;
+
+ return (
+
+ );
+}
diff --git a/src/components/FeaturedVideos/VideoListItem.tsx b/src/components/FeaturedVideos/VideoListItem.tsx
new file mode 100644
index 000000000..31aa4bd67
--- /dev/null
+++ b/src/components/FeaturedVideos/VideoListItem.tsx
@@ -0,0 +1,38 @@
+import type { VideoFileType } from '../../lib/video';
+
+export interface VideoListItemProps {
+ video: VideoFileType;
+}
+
+export function VideoListItem(props: VideoListItemProps) {
+ const { video } = props;
+ const { frontmatter, id } = video;
+
+ return (
+
+
+ {frontmatter.title}
+
+ {frontmatter.isNew && (
+
+ New
+
+ ·
+ {new Date(frontmatter.date).toLocaleString('default', {
+ month: 'long',
+ })}
+
+
+ )}
+
+
+ {frontmatter.duration}
+
+
+ »
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/GuideListItem.astro b/src/components/GuideListItem.astro
deleted file mode 100644
index a862ad354..000000000
--- a/src/components/GuideListItem.astro
+++ /dev/null
@@ -1,61 +0,0 @@
----
-import type { GuideFileType, GuideFrontmatter } from '../lib/guide';
-import { type QuestionGroupType } from '../lib/question-group';
-
-export interface Props {
- guide: GuideFileType | QuestionGroupType;
-}
-
-function isQuestionGroupType(
- guide: GuideFileType | QuestionGroupType,
-): guide is QuestionGroupType {
- return (guide as QuestionGroupType).questions !== undefined;
-}
-
-const { guide } = Astro.props;
-const { frontmatter, id } = guide;
-
-let pageUrl = '';
-let guideType = '';
-
-if (isQuestionGroupType(guide)) {
- pageUrl = `/questions/${id}`;
- guideType = 'Questions';
-} else {
- const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
- pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
- guideType = (frontmatter as GuideFrontmatter).type;
-}
----
-
-
-
- {frontmatter.title}
-
- {
- frontmatter.isNew && (
-
- New
-
- ·
- {new Date(frontmatter.date || '').toLocaleString('default', {
- month: 'long',
- })}
-
-
- )
- }
-
-
- {guideType}
-
-
- »
-
diff --git a/src/components/HeroSection/FavoriteRoadmaps.tsx b/src/components/HeroSection/FavoriteRoadmaps.tsx
index a639be626..d2c585518 100644
--- a/src/components/HeroSection/FavoriteRoadmaps.tsx
+++ b/src/components/HeroSection/FavoriteRoadmaps.tsx
@@ -1,164 +1,229 @@
-import { useEffect, useState } from 'react';
-import { EmptyProgress } from './EmptyProgress';
-import { httpGet } from '../../lib/http';
-import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
-import { isLoggedIn } from '../../lib/jwt';
-import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
-
-export type UserProgressResponse = {
- resourceId: string;
- resourceType: 'roadmap' | 'best-practice';
- resourceTitle: string;
- isFavorite: boolean;
- done: number;
- learning: number;
- skipped: number;
- total: number;
- updatedAt: Date;
- isCustomResource: boolean;
- roadmapSlug?: string;
- team?: {
- name: string;
- id: string;
- role: AllowedMemberRoles;
- };
-}[];
-
-function renderProgress(progressList: UserProgressResponse) {
- progressList.forEach((progress) => {
- const href =
- progress.resourceType === 'best-practice'
- ? `/best-practices/${progress.resourceId}`
- : `/${progress.resourceId}`;
- const element = document.querySelector(`a[href="${href}"]`);
- if (!element) {
- return;
- }
-
- window.dispatchEvent(
- new CustomEvent('mark-favorite', {
- detail: {
- resourceId: progress.resourceId,
- resourceType: progress.resourceType,
- isFavorite: progress.isFavorite,
- },
- }),
- );
-
- const totalDone = progress.done + progress.skipped;
- const percentageDone = (totalDone / progress.total) * 100;
-
- const progressBar: HTMLElement | null =
- element.querySelector('[data-progress]');
- if (progressBar) {
- progressBar.style.width = `${percentageDone}%`;
- }
- });
-}
-
-type ProgressResponse = UserProgressResponse;
-
-export function FavoriteRoadmaps() {
- const isAuthenticated = isLoggedIn();
- if (!isAuthenticated) {
- return null;
- }
-
- const [isPreparing, setIsPreparing] = useState(true);
- const [isLoading, setIsLoading] = useState(true);
- const [progress, setProgress] = useState
([]);
- const [containerOpacity, setContainerOpacity] = useState(0);
-
- function showProgressContainer() {
- const heroEl = document.getElementById('hero-text')!;
- if (!heroEl) {
- return;
- }
-
- heroEl.classList.add('opacity-0');
- setTimeout(() => {
- heroEl.parentElement?.removeChild(heroEl);
- setIsPreparing(false);
-
- setTimeout(() => {
- setContainerOpacity(100);
- }, 50);
- }, 0);
- }
-
- async function loadProgress() {
- setIsLoading(true);
-
- const { response: progressList, error } = await httpGet(
- `${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
- );
-
- if (error || !progressList) {
- return;
- }
-
- setProgress(progressList);
- setIsLoading(false);
- showProgressContainer();
-
- // render progress on featured items
- renderProgress(progressList);
- }
-
- useEffect(() => {
- loadProgress().finally(() => {
- setIsLoading(false);
- });
- }, []);
-
- useEffect(() => {
- window.addEventListener('refresh-favorites', loadProgress);
- return () => window.removeEventListener('refresh-favorites', loadProgress);
- }, []);
-
- if (isPreparing) {
- return null;
- }
-
- const hasProgress = progress?.length > 0;
- const customRoadmaps = progress?.filter(
- (p) => p.isCustomResource && !p.team?.name,
+import {
+ FolderKanban,
+ MapIcon,
+ Plus,
+ Sparkle,
+ Eye,
+ EyeOff,
+ Square,
+ SquareCheckBig,
+} from 'lucide-react';
+import { useState } from 'react';
+import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
+import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
+import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx';
+import { HeroProject } from './HeroProject';
+import { HeroRoadmap } from './HeroRoadmap';
+import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx';
+import { HeroItemsGroup } from './HeroItemsGroup';
+import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
+
+export type AIRoadmapType = {
+ id: string;
+ title: string;
+ slug: string;
+};
+
+type FavoriteRoadmapsProps = {
+ progress: UserProgress[];
+ projects: (ProjectStatusDocument & {
+ title: string;
+ })[];
+ customRoadmaps: UserProgress[];
+ aiRoadmaps: AIRoadmapType[];
+ isLoading: boolean;
+};
+
+export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
+ const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
+ const [showCompleted, setShowCompleted] = useState(false);
+ const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
+
+ const completedProjects = projects.filter(
+ (project) => project.submittedAt && project.repositoryUrl,
+ );
+ const inProgressProjects = projects.filter(
+ (project) => !project.submittedAt || !project.repositoryUrl,
);
- const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
- const teamRoadmaps: HeroTeamRoadmaps = progress
- ?.filter((p) => p.isCustomResource && p.team?.name)
- .reduce((acc: HeroTeamRoadmaps, curr) => {
- const currTeam = curr.team!;
- if (!acc[currTeam.name]) {
- acc[currTeam.name] = [];
- }
-
- acc[currTeam.name].push(curr);
- return acc;
- }, {});
+ const projectsToShow = [
+ ...inProgressProjects,
+ ...(showCompleted ? completedProjects : []),
+ ];
return (
-
-
+ {isCreatingCustomRoadmap && (
+
{
+ setIsCreatingCustomRoadmap(false);
+ }}
+ />
+ )}
+
+ }
+ isLoading={isLoading}
+ title="Your progress and bookmarks"
+ isEmpty={!isLoading && progress.length === 0}
+ emptyTitle={
+ <>
+ No bookmars found
+
+
+ Bookmark a roadmap
+
+ >
+ }
+ >
+ {progress.map((resource) => (
+
+ ))}
+
+
+ }
+ isLoading={isLoading}
+ title="Your custom roadmaps"
+ isEmpty={!isLoading && customRoadmaps.length === 0}
+ emptyTitle={
+ <>
+ No custom roadmaps found
+
+ >
+ }
+ >
+ {customRoadmaps.map((customRoadmap) => (
+
+ ))}
+
+
+
+ }
+ isLoading={isLoading}
+ title="Your AI roadmaps"
+ isEmpty={!isLoading && aiRoadmaps.length === 0}
+ emptyTitle={
+ <>
+ No AI roadmaps found
+
+
+ Generate AI roadmap
+
+ >
+ }
+ >
+ {aiRoadmaps.map((aiRoadmap) => (
+
+ ))}
+
+
+
+ Generate New
+
+
+
+ }
+ isLoading={isLoading}
+ title="Your active projects"
+ isEmpty={!isLoading && projectsToShow.length === 0}
+ emptyTitle={
+ <>
+ No active projects found
+
+
+ Start a new project
+
+ >
+ }
+ rightContent={
+ completedProjects.length > 0 && (
+
+ )
+ }
+ className="border-b-0"
>
-
- {!isLoading && progress?.length == 0 && }
- {hasProgress && (
-
- )}
-
-
+ {projectsToShow.map((project) => (
+
+ ))}
+
+
+
+ Start a new project
+
+
);
}
diff --git a/src/components/HeroSection/HeroItemsGroup.tsx b/src/components/HeroSection/HeroItemsGroup.tsx
new file mode 100644
index 000000000..f4e9b46f5
--- /dev/null
+++ b/src/components/HeroSection/HeroItemsGroup.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useRef, useState, type ReactNode } from 'react';
+import { cn } from '../../lib/classname';
+import { HeroTitle } from './HeroTitle';
+
+type HeroItemsGroupProps = {
+ icon: any;
+ isLoading?: boolean;
+ isEmpty?: boolean;
+ emptyTitle?: ReactNode;
+ title: string | ReactNode;
+ rightContent?: ReactNode;
+ children?: ReactNode;
+ className?: string;
+};
+
+export function HeroItemsGroup(props: HeroItemsGroupProps) {
+ const {
+ icon,
+ isLoading = false,
+ isEmpty = false,
+ emptyTitle,
+ title,
+ rightContent,
+ children,
+ className,
+ } = props;
+
+ const storageKey = `hero-group-${title}-collapsed`;
+ const [isCollapsed, setIsCollapsed] = useState(true);
+
+ function isCollapsedByStorage() {
+ const stored = localStorage.getItem(storageKey);
+
+ return stored === 'true';
+ }
+
+ useEffect(() => {
+ setIsCollapsed(isCollapsedByStorage());
+ }, [isLoading]);
+
+ const isLoadingOrCollapsedOrEmpty = isLoading || isCollapsed || isEmpty;
+
+ return (
+
+
+
{
+ setIsCollapsed(!isCollapsed);
+ localStorage.setItem(storageKey, (!isCollapsed).toString());
+ }}
+ />
+ {!isLoadingOrCollapsedOrEmpty && (
+
+ {children}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/HeroSection/HeroProject.tsx b/src/components/HeroSection/HeroProject.tsx
new file mode 100644
index 000000000..c86f08076
--- /dev/null
+++ b/src/components/HeroSection/HeroProject.tsx
@@ -0,0 +1,52 @@
+import { ThumbsUp } from 'lucide-react';
+import { cn } from '../../lib/classname.ts';
+import { getRelativeTimeString } from '../../lib/date';
+import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
+
+type HeroProjectProps = {
+ project: ProjectStatusDocument & {
+ title: string;
+ };
+};
+
+export function HeroProject({ project }: HeroProjectProps) {
+ return (
+
+
+
+ {project.title}
+
+
+ {project.submittedAt && project.repositoryUrl ? 'Done' : ''}
+
+
+
+ {project.submittedAt && project.repositoryUrl && (
+
+
+ {project.upvotes}
+
+ )}
+ {project.startedAt && (
+ Started {getRelativeTimeString(project.startedAt)}
+ )}
+
+
+
+ {project.submittedAt && project.repositoryUrl && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/HeroSection/HeroRoadmap.tsx b/src/components/HeroSection/HeroRoadmap.tsx
new file mode 100644
index 000000000..c8f15ad2e
--- /dev/null
+++ b/src/components/HeroSection/HeroRoadmap.tsx
@@ -0,0 +1,74 @@
+import { cn } from '../../lib/classname.ts';
+import type { ResourceType } from '../../lib/resource-progress.ts';
+import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
+
+type ProgressRoadmapProps = {
+ url: string;
+ percentageDone: number;
+ allowFavorite?: boolean;
+
+ resourceId: string;
+ resourceType: ResourceType;
+ resourceTitle: string;
+ isFavorite?: boolean;
+
+ isTrackable?: boolean;
+ isNew?: boolean;
+};
+
+export function HeroRoadmap(props: ProgressRoadmapProps) {
+ const {
+ url,
+ percentageDone,
+ resourceType,
+ resourceId,
+ resourceTitle,
+ isFavorite,
+ allowFavorite = true,
+ isTrackable = true,
+ isNew = false,
+ } = props;
+
+ return (
+
+
+ {resourceTitle}
+
+
+ {isTrackable && (
+
+ )}
+
+ {allowFavorite && (
+
+ )}
+
+ {isNew && (
+
+
+
+
+
+ New
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/HeroSection/HeroRoadmaps.tsx b/src/components/HeroSection/HeroRoadmaps.tsx
deleted file mode 100644
index 7633f0ab7..000000000
--- a/src/components/HeroSection/HeroRoadmaps.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-import type { UserProgressResponse } from './FavoriteRoadmaps';
-import { CheckIcon } from '../ReactIcons/CheckIcon';
-import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
-import { Spinner } from '../ReactIcons/Spinner';
-import type { ResourceType } from '../../lib/resource-progress';
-import { MapIcon, Users2 } from 'lucide-react';
-import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
-import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
-import { type ReactNode, useState } from 'react';
-import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx';
-
-type ProgressRoadmapProps = {
- url: string;
- percentageDone: number;
- allowFavorite?: boolean;
-
- resourceId: string;
- resourceType: ResourceType;
- resourceTitle: string;
- isFavorite?: boolean;
-};
-function HeroRoadmap(props: ProgressRoadmapProps) {
- const {
- url,
- percentageDone,
- resourceType,
- resourceId,
- resourceTitle,
- isFavorite,
- allowFavorite = true,
- } = props;
-
- return (
-
- {resourceTitle}
-
-
-
- {allowFavorite && (
-
- )}
-
- );
-}
-
-type ProgressTitleProps = {
- icon: any;
- isLoading?: boolean;
- title: string | ReactNode;
-};
-
-export function HeroTitle(props: ProgressTitleProps) {
- const { isLoading = false, title, icon } = props;
-
- return (
-
- {!isLoading && icon}
- {isLoading && (
-
-
-
- )}
- {title}
-
- );
-}
-export type HeroTeamRoadmaps = Record;
-
-type ProgressListProps = {
- progress: UserProgressResponse;
- customRoadmaps: UserProgressResponse;
- teamRoadmaps?: HeroTeamRoadmaps;
- isLoading?: boolean;
-};
-
-export function HeroRoadmaps(props: ProgressListProps) {
- const {
- teamRoadmaps = {},
- progress,
- isLoading = false,
- customRoadmaps,
- } = props;
-
- const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
- const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState();
-
- return (
-
-
-
-
- {isCreatingRoadmap && (
-
{
- setIsCreatingRoadmap(false);
- setCreatingRoadmapTeamId(undefined);
- }}
- />
- )}
- {
- ) as any
- }
- isLoading={isLoading}
- title="Your progress and favorite roadmaps."
- />
- }
-
-
- {progress.map((resource) => (
-
- ))}
-
-
-
- {
-
}
- title="Your custom roadmaps"
- />
- }
-
- {customRoadmaps.length === 0 && (
-
- You haven't created any custom roadmaps yet.{' '}
-
-
- )}
-
- {customRoadmaps.length > 0 && (
-
- {customRoadmaps.map((customRoadmap) => {
- return (
-
- );
- })}
-
-
-
- )}
-
-
- {Object.keys(teamRoadmaps).map((teamName) => {
- const currentTeam: UserProgressResponse[0]['team'] =
- teamRoadmaps?.[teamName]?.[0]?.team;
- const roadmapsList = teamRoadmaps[teamName].filter(
- (roadmap) => !!roadmap.resourceTitle,
- );
- const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
-
- return (
-
- {
-
}
- title={
- <>
- Team{' '}
-
- {teamName}
-
- Roadmaps
- >
- }
- />
- }
-
- {roadmapsList.length === 0 && (
-
- Team does not have any roadmaps yet.{' '}
- {canManageTeam && (
-
- )}
-
- )}
-
- {roadmapsList.length > 0 && (
-
- {roadmapsList.map((customRoadmap) => {
- return (
-
- );
- })}
-
- {canManageTeam && (
-
- )}
-
- )}
-
- );
- })}
-
- );
-}
diff --git a/src/components/HeroSection/HeroTitle.tsx b/src/components/HeroSection/HeroTitle.tsx
new file mode 100644
index 000000000..7b2173f27
--- /dev/null
+++ b/src/components/HeroSection/HeroTitle.tsx
@@ -0,0 +1,71 @@
+import type { ReactNode } from 'react';
+import { Spinner } from '../ReactIcons/Spinner.tsx';
+import { ChevronDown, ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
+import { cn } from '../../lib/classname.ts';
+
+type HeroTitleProps = {
+ icon: any;
+ isLoading?: boolean;
+ title: string | ReactNode;
+ rightContent?: ReactNode;
+ isCollapsed?: boolean;
+ onToggleCollapse?: () => void;
+ isEmpty?: boolean;
+ emptyTitle?: ReactNode;
+};
+
+export function HeroTitle(props: HeroTitleProps) {
+ const {
+ isLoading = false,
+ title,
+ icon,
+ rightContent,
+ isCollapsed = false,
+ onToggleCollapse,
+ isEmpty = false,
+ emptyTitle,
+ } = props;
+
+ return (
+
+
+
+ {!isLoading && icon}
+ {isLoading && (
+
+
+
+ )}
+ {!isEmpty ? title : emptyTitle || title}
+
+
+
+ {!isCollapsed && rightContent}
+
+ {!isLoading && !isEmpty && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/RoadCard/RoadmapSelect.tsx b/src/components/RoadCard/RoadmapSelect.tsx
index 2db7bc2e4..baa7fae22 100644
--- a/src/components/RoadCard/RoadmapSelect.tsx
+++ b/src/components/RoadCard/RoadmapSelect.tsx
@@ -1,8 +1,8 @@
import { httpGet } from '../../lib/http';
import { useEffect, useState } from 'react';
import { pageProgressMessage } from '../../stores/page';
-import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
import { SelectionButton } from './SelectionButton';
+import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage';
type RoadmapSelectProps = {
selectedRoadmaps: string[];
diff --git a/src/components/Roadmaps/RoadmapsPage.tsx b/src/components/Roadmaps/RoadmapsPage.tsx
index defe3e3e8..91b6a72e1 100644
--- a/src/components/Roadmaps/RoadmapsPage.tsx
+++ b/src/components/Roadmaps/RoadmapsPage.tsx
@@ -10,8 +10,27 @@ import {
} from '../../lib/browser.ts';
import { RoadmapCard } from './RoadmapCard.tsx';
import { httpGet } from '../../lib/http.ts';
-import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx';
import { isLoggedIn } from '../../lib/jwt.ts';
+import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
+
+export type UserProgressResponse = {
+ resourceId: string;
+ resourceType: 'roadmap' | 'best-practice';
+ resourceTitle: string;
+ isFavorite: boolean;
+ done: number;
+ learning: number;
+ skipped: number;
+ total: number;
+ updatedAt: Date;
+ isCustomResource: boolean;
+ roadmapSlug?: string;
+ team?: {
+ name: string;
+ id: string;
+ role: AllowedMemberRoles;
+ };
+}[];
const groupNames = [
'Absolute Beginners',
diff --git a/src/components/TeamProgress/TeamProgressPage.tsx b/src/components/TeamProgress/TeamProgressPage.tsx
index 78e8c7585..74784f959 100644
--- a/src/components/TeamProgress/TeamProgressPage.tsx
+++ b/src/components/TeamProgress/TeamProgressPage.tsx
@@ -1,16 +1,15 @@
+import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
+import { useAuth } from '../../hooks/use-auth';
+import { useToast } from '../../hooks/use-toast';
+import { getUrlParams, setUrlParams } from '../../lib/browser';
import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
-import { MemberProgressItem } from './MemberProgressItem';
-import { useToast } from '../../hooks/use-toast';
-import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem';
-import { getUrlParams, setUrlParams } from '../../lib/browser';
-import { useAuth } from '../../hooks/use-auth';
-import { MemberProgressModal } from './MemberProgressModal';
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
-import { canManageCurrentRoadmap } from '../../stores/roadmap.ts';
+import { MemberProgressItem } from './MemberProgressItem';
+import { MemberProgressModal } from './MemberProgressModal';
export type UserProgress = {
resourceTitle: string;
diff --git a/src/components/VideoListItem.astro b/src/components/VideoListItem.astro
deleted file mode 100644
index a95bf4905..000000000
--- a/src/components/VideoListItem.astro
+++ /dev/null
@@ -1,40 +0,0 @@
----
-import type { VideoFileType } from '../lib/video';
-
-export interface Props {
- video: VideoFileType;
-}
-
-const { video } = Astro.props;
-const { frontmatter, id } = video;
----
-
-
-
- {frontmatter.title}
-
- {
- frontmatter.isNew && (
-
- New
-
- ·
- {new Date(frontmatter.date).toLocaleString('default', {
- month: 'long',
- })}
-
-
- )
- }
-
-
- {frontmatter.duration}
-
-
- »
-
diff --git a/src/lib/question-group.ts b/src/lib/question-group.ts
index 68950ed4c..57080cb11 100644
--- a/src/lib/question-group.ts
+++ b/src/lib/question-group.ts
@@ -3,7 +3,7 @@ import slugify from 'slugify';
import { getAllAuthors, type AuthorFileType } from './author.ts';
import { getAllGuides } from './guide.ts';
-interface RawQuestionGroupFrontmatter {
+export interface RawQuestionGroupFrontmatter {
order: number;
briefTitle: string;
briefDescription: string;
diff --git a/src/pages/authors/[authorId].astro b/src/pages/authors/[authorId].astro
index 0ed281319..2bbcd920d 100644
--- a/src/pages/authors/[authorId].astro
+++ b/src/pages/authors/[authorId].astro
@@ -1,12 +1,12 @@
---
-import BaseLayout from '../../layouts/BaseLayout.astro';
import AstroIcon from '../../components/AstroIcon.astro';
-import { getGuidesByAuthor } from '../../lib/guide';
-import { getVideosByAuthor } from '../../lib/video';
-import GuideListItem from '../../components/GuideListItem.astro';
+import { GuideListItem } from '../../components/FeaturedGuides/GuideListItem';
+import { VideoListItem } from '../../components/FeaturedVideos/VideoListItem';
+import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAuthorById, getAuthorIds } from '../../lib/author';
-import VideoListItem from '../../components/VideoListItem.astro';
+import { getGuidesByAuthor } from '../../lib/guide';
import { getAllQuestionGroups } from '../../lib/question-group';
+import { getVideosByAuthor } from '../../lib/video';
interface Params extends Record {}
@@ -136,9 +136,12 @@ const videos = await getVideosByAuthor(authorId);
{
[...guides, ...questionGuides]
.sort((a, b) => {
- const aDate = a.frontmatter.date || a.frontmatter.publishedAt;
- const bDate = b.frontmatter.date || b.frontmatter.publishedAt;
- return new Date(bDate) - new Date(aDate);
+ const aFrontmatter = a.frontmatter as any;
+ const bFrontmatter = b.frontmatter as any;
+
+ const aDate = aFrontmatter.date || aFrontmatter.publishedAt;
+ const bDate = bFrontmatter.date || bFrontmatter.publishedAt;
+ return new Date(bDate).getTime() - new Date(aDate).getTime();
})
.map((guide) => )
}
diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro
index 50d8afcce..618550470 100644
--- a/src/pages/dashboard.astro
+++ b/src/pages/dashboard.astro
@@ -2,11 +2,17 @@
import { DashboardPage } from '../components/Dashboard/DashboardPage';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getAllBestPractices } from '../lib/best-practice';
+import { getAllQuestionGroups } from '../lib/question-group';
import { getRoadmapsByTag } from '../lib/roadmap';
+import { getAllGuides } from '../lib/guide';
+import { getAllVideos } from '../lib/video';
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
const bestPractices = await getAllBestPractices();
+const questionGroups = await getAllQuestionGroups();
+const guides = await getAllGuides();
+const videos = await getAllVideos();
const enrichedRoleRoadmaps = roleRoadmaps
.filter((roadmapItem) => !roadmapItem.frontmatter.isHidden)
@@ -20,6 +26,7 @@ const enrichedRoleRoadmaps = roleRoadmaps
description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer,
+ isNew: frontmatter.isNew,
metadata: {
tags: frontmatter.tags,
},
@@ -38,6 +45,7 @@ const enrichedSkillRoadmaps = skillRoadmaps
description: frontmatter.briefDescription,
relatedRoadmapIds: frontmatter.relatedRoadmaps,
renderer: frontmatter.renderer,
+ isNew: frontmatter.isNew,
metadata: {
tags: frontmatter.tags,
},
@@ -56,13 +64,16 @@ const enrichedBestPractices = bestPractices.map((bestPractice) => {
});
---
-
+
-
+
diff --git a/src/pages/guides/index.astro b/src/pages/guides/index.astro
index df27054ff..ac7eeb02e 100644
--- a/src/pages/guides/index.astro
+++ b/src/pages/guides/index.astro
@@ -1,5 +1,5 @@
---
-import GuideListItem from '../../components/GuideListItem.astro';
+import { GuideListItem } from '../../components/FeaturedGuides/GuideListItem';
import SimplePageHeader from '../../components/SimplePageHeader.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllGuides } from '../../lib/guide';
@@ -12,8 +12,8 @@ const questionGuides = (await getAllQuestionGroups()).filter(
const allGuides = [...guides, ...questionGuides];
const sortedGuides = allGuides.sort((a, b) => {
- const aDate = new Date(a.frontmatter.date);
- const bDate = new Date(b.frontmatter.date);
+ const aDate = new Date(a.frontmatter.date as string);
+ const bDate = new Date(b.frontmatter.date as string);
return bDate.getTime() - aDate.getTime();
});
@@ -36,5 +36,5 @@ const sortedGuides = allGuides.sort((a, b) => {