1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-08-20 08:02:35 +02:00

feat: featured guide list

This commit is contained in:
Arik Chakma
2025-08-18 19:53:56 +06:00
parent e78ef36d9e
commit 825ea021a4
7 changed files with 45 additions and 280 deletions

View File

@@ -1,22 +1,18 @@
import type { GuideFileType } from '../../lib/guide';
import type { QuestionGroupType } from '../../lib/question-group';
import type { OfficialGuideDocument } from '../../queries/official-guide';
import { GuideListItem } from './GuideListItem';
export interface FeaturedGuidesProps {
heading: string;
guides: GuideFileType[];
questions: QuestionGroupType[];
guides: OfficialGuideDocument[];
questions: OfficialGuideDocument[];
}
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);
const sortedGuides = [...guides, ...questions].sort((a, b) => {
const aDate = new Date(a.publishedAt ?? new Date());
const bDate = new Date(b.publishedAt ?? new Date());
return bDate.getTime() - aDate.getTime();
});
@@ -27,7 +23,7 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
<div className="mt-3 sm:my-5">
{sortedGuides.map((guide) => (
<GuideListItem key={guide.id} guide={guide} />
<GuideListItem key={guide._id} guide={guide} />
))}
</div>

View File

@@ -1,52 +1,46 @@
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
import { type QuestionGroupType } from '../../lib/question-group';
import dayjs from 'dayjs';
import { DateTime } from 'luxon';
import {
getOfficialGuideHref,
type OfficialGuideDocument,
} from '../../queries/official-guide';
export interface GuideListItemProps {
guide: GuideFileType | QuestionGroupType;
}
function isQuestionGroupType(
guide: GuideFileType | QuestionGroupType,
): guide is QuestionGroupType {
return (guide as QuestionGroupType).questions !== undefined;
guide: OfficialGuideDocument;
}
export function GuideListItem(props: GuideListItemProps) {
const { guide } = props;
const { frontmatter, id } = guide;
const { title, slug, publishedAt, roadmapId } = 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;
let guideType = 'Textual';
if (roadmapId === 'questions') {
guideType = 'Question';
}
// Check if article is within the last 15 days
const isNew = frontmatter.date
? dayjs().diff(dayjs(frontmatter.date), 'day') < 15
: false;
const publishedAtDate = publishedAt
? DateTime.fromJSDate(new Date(publishedAt))
: null;
const isNew =
publishedAtDate && DateTime.now().diff(publishedAtDate, 'days').days < 15;
const publishedAtMonth = publishedAtDate
? publishedAtDate.toFormat('MMMM')
: '';
return (
<a
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={pageUrl}
className="text-md group flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={getOfficialGuideHref(slug, roadmapId)}
>
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
{frontmatter.title}
{title}
{isNew && (
<span className="ml-2.5 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase">
New
<span className="hidden sm:inline">
&nbsp;&middot;&nbsp;
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
{publishedAtMonth}
</span>
</span>
)}

View File

@@ -1,153 +0,0 @@
---
import AstroIcon from '../../components/AstroIcon.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 { getGuidesByAuthor } from '../../lib/guide';
import { getAllQuestionGroups } from '../../lib/question-group';
import { getVideosByAuthor } from '../../lib/video';
export const prerender = true;
interface Params extends Record<string, string | undefined> {}
export async function getStaticPaths() {
const authorIds = await getAuthorIds();
return authorIds.map((authorId) => ({
params: { authorId },
}));
}
const { authorId } = Astro.params;
const author = await getAuthorById(authorId);
const authorFrontmatter = author.frontmatter;
const guides = await getGuidesByAuthor(authorId);
const questionGuides = (await getAllQuestionGroups()).filter(
(group) => group.frontmatter.authorId === authorId,
);
const videos = await getVideosByAuthor(authorId);
---
<BaseLayout
permalink={`/authors/${author.id}`}
title={`${author.frontmatter.name} - Author at roadmap.sh`}
briefTitle={author.frontmatter.name}
ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`}
description={`${author.frontmatter.name} has written ${guides.length} articles on roadmap.sh on a variety of topics.`}
noIndex={false}
jsonLd={[
{
'@context': 'https://schema.org/',
'@type': 'Person',
name: authorFrontmatter.name,
url: `https://roadmap.sh/authors/${authorId}`,
image: authorFrontmatter.imageUrl.startsWith('http')
? authorFrontmatter.imageUrl
: `https://roadmap.sh${authorFrontmatter.imageUrl}`,
sameAs: authorFrontmatter.social
? Object.values(authorFrontmatter.social)
: [],
...(authorFrontmatter.employment && {
jobTitle: authorFrontmatter.employment?.title,
worksFor: {
'@type': 'Organization',
name: authorFrontmatter.employment.company,
},
}),
},
]}
>
<div class='container pb-0 pt-4 md:pb-16 md:pt-8'>
<div class=''>
<div class='mb-5 flex items-center gap-8 rounded-3xl py-0 md:py-8'>
<div class='grow'>
<h1 class='text-2xl font-bold md:text-3xl'>
{authorFrontmatter.name}
</h1>
<div
class='mb-4 mt-1 flex flex-col gap-3 leading-normal text-gray-800 md:mb-6 md:mt-4 [&>p>a]:font-semibold [&>p>a]:underline'
>
<author.Content />
</div>
<div class='flex items-center justify-between'>
<div class='flex items-center gap-1.5'>
{
authorFrontmatter.social?.github && (
<a
href={authorFrontmatter.social.github}
target='_blank'
class='text-gray-500 transition-colors hover:text-gray-800'
>
<AstroIcon icon='github' class='h-[20px]' />
</a>
)
}
{
authorFrontmatter.social.twitter && (
<a
href={authorFrontmatter.social.twitter}
target='_blank'
class='text-gray-500 transition-colors hover:text-gray-800'
>
<AstroIcon icon='twitter' class='h-[20px]' />
</a>
)
}
{
authorFrontmatter.social.linkedin && (
<a
href={authorFrontmatter.social.linkedin}
target='_blank'
class='text-gray-500 transition-colors hover:text-gray-800'
>
<AstroIcon icon='linkedin-2' class='h-[20px]' />
</a>
)
}
{
authorFrontmatter.social.website && (
<a
href={authorFrontmatter.social.website}
target='_blank'
class='text-gray-500 transition-colors hover:text-gray-800'
>
<AstroIcon icon='globe' class='h-[20px]' />
</a>
)
}
</div>
</div>
</div>
<div class='hidden shrink-0 flex-col md:flex'>
<img
alt="Kamran Ahmed's profile picture"
class='block h-[175px] w-[175px] rounded-full bg-gray-100'
src={authorFrontmatter.imageUrl}
/>
</div>
</div>
</div>
<div
class='rounded-t-xl bg-linear-to-b from-gray-100 to-white px-3 py-2 md:px-6 md:py-3 [&>*:last-child]:border-b-0'
>
{
[...guides, ...questionGuides]
.sort((a, b) => {
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) => <GuideListItem guide={guide} />)
}
{videos.map((video) => <VideoListItem video={video} />)}
</div>
</div>
</BaseLayout>

View File

@@ -1,30 +0,0 @@
import type { APIRoute } from 'astro';
import { getAuthorById, getAuthorIds } from '../../lib/author';
export const prerender = true;
export async function getStaticPaths() {
const authorIds = await getAuthorIds();
return await Promise.all(
authorIds.map(async (authorId) => {
const authorDetails = await getAuthorById(authorId);
return {
params: { authorId },
props: {
authorDetails: authorDetails?.frontmatter || {},
},
};
}),
);
}
export const GET: APIRoute = async function ({ params, request, props }) {
return new Response(JSON.stringify(props.authorDetails), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
};

View File

@@ -1,40 +0,0 @@
---
import { GuideListItem } from '../../components/FeaturedGuides/GuideListItem';
import SimplePageHeader from '../../components/SimplePageHeader.astro';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllGuides } from '../../lib/guide';
import { getAllQuestionGroups } from '../../lib/question-group';
const guides = await getAllGuides();
const questionGuides = (await getAllQuestionGroups()).filter(
(questionGroup) => questionGroup.frontmatter.authorId,
);
const allGuides = [...guides, ...questionGuides];
const sortedGuides = allGuides.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();
});
---
<BaseLayout
title='Guides - roadmap.sh'
description={'Detailed guides on Software Engineering Topics'}
permalink={`/guides`}
>
<SimplePageHeader
title='Guides'
description='Succinct graphical explanations to engineering topics.'
/>
<div class='bg-gray-50 pb-20 pt-2'>
<div class='container'>
<div class='mt-3 sm:my-5'>
{sortedGuides.map((guide) => <GuideListItem guide={guide} />)}
</div>
</div>
</div>
<div slot='changelog-banner'></div>
</BaseLayout>

View File

@@ -6,15 +6,13 @@ import { FeaturedVideoList } from '../components/FeaturedVideos/FeaturedVideoLis
import HeroSection from '../components/HeroSection/HeroSection.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { getAllBestPractices } from '../lib/best-practice';
import { getAllGuides } from '../lib/guide';
import { getAllQuestionGroups } from '../lib/question-group';
import { getRoadmapsByTag } from '../lib/roadmap';
import { getAllVideos } from '../lib/video';
import { listOfficialGuides } from '../queries/official-guide';
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
const bestPractices = await getAllBestPractices();
const questionGroups = await getAllQuestionGroups();
export const projectGroups = [
{
@@ -31,9 +29,9 @@ export const projectGroups = [
},
];
const guides = await getAllGuides();
const questionGuides = (await getAllQuestionGroups()).filter(
(questionGroup) => questionGroup.frontmatter.authorId,
const guides = await listOfficialGuides();
const questionGuides = guides.filter(
(guide) => guide.roadmapId === 'questions' && !!guide?.authorId,
);
const videos = await getAllVideos();
---
@@ -97,16 +95,6 @@ const videos = await getAllVideos();
}))}
/>
<FeaturedItems
heading='Questions'
allowBookmark={false}
featuredItems={questionGroups.map((questionGroup) => ({
text: questionGroup.frontmatter.briefTitle,
url: `/questions/${questionGroup.id}`,
isNew: questionGroup.frontmatter.isNew,
}))}
/>
<div class='grid grid-cols-1 gap-7 bg-gray-50 py-7 sm:gap-16 sm:py-16'>
<FeaturedGuideList
heading='Guides'

View File

@@ -32,6 +32,7 @@ export interface OfficialGuideDocument {
type ListOfficialGuidesQuery = {
authorSlug?: string;
roadmapId?: string;
};
export async function listOfficialGuides(query: ListOfficialGuidesQuery = {}) {
@@ -101,3 +102,12 @@ export async function getOfficialGuide(slug: string, roadmapId?: string) {
throw error;
}
}
export function getOfficialGuideHref(slug: string, roadmapId?: string) {
const isExternal = roadmapId && roadmapId !== 'questions';
return isExternal
? `${import.meta.env.PUBLIC_APP_URL}/${roadmapId}/${slug}`
: roadmapId
? `/${roadmapId}/${slug}`
: `/guides/${slug}`;
}