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:
@@ -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>
|
||||
|
||||
@@ -48,4 +44,4 @@ export function FeaturedGuideList(props: FeaturedGuidesProps) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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">
|
||||
·
|
||||
{frontmatter.date ? dayjs(frontmatter.date).format('MMMM') : ''}
|
||||
{publishedAtMonth}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
@@ -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>
|
@@ -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',
|
||||
},
|
||||
});
|
||||
};
|
@@ -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>
|
@@ -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'
|
||||
|
@@ -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}`;
|
||||
}
|
||||
|
Reference in New Issue
Block a user