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

Add guides v2

This commit is contained in:
Kamran Ahmed
2025-08-13 00:53:15 +01:00
parent 404859737d
commit 61a5cb8137
3 changed files with 473 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import SimplePageHeader from '../../components/SimplePageHeader.astro';
import { httpGet } from '../../lib/http';
export async function getStaticPaths() {
// For now, return an empty array for static generation
// In production, you'd fetch all authors here
return [];
}
const { authorSlug } = Astro.params;
const apiUrl = import.meta.env.PUBLIC_API_URL || 'http://localhost:8080';
// Fetch author details
const { response: author, error: authorError } = await httpGet(
`${apiUrl}/v1-get-author/${authorSlug}`
);
if (authorError || !author) {
return Astro.redirect('/404');
}
// Fetch author's guides
const { response: guidesData, error: guidesError } = await httpGet(
`${apiUrl}/v1-list-guides`,
{
authorId: author._id,
perPage: 100,
sortBy: '-publishedAt',
}
);
const guides = guidesData?.data || [];
// Check if guide is within the last 15 days
const isNew = (publishedAt: string) => {
if (!publishedAt) return false;
const daysDiff = Math.floor((Date.now() - new Date(publishedAt).getTime()) / (1000 * 60 * 60 * 24));
return daysDiff < 15;
};
---
<BaseLayout
title={`${author.name} - Author at roadmap.sh`}
description={author.bio || `Guides and articles by ${author.name}`}
permalink={`/authors/${author.slug}`}
>
<div class='bg-white py-8'>
<div class='container'>
<div class='mx-auto max-w-4xl'>
<!-- Author Header -->
<div class='mb-8 border-b pb-8'>
<div class='flex items-start space-x-6'>
{author.avatar && (
<img
src={author.avatar}
alt={author.name}
class='h-24 w-24 rounded-full'
/>
)}
<div class='flex-1'>
<h1 class='mb-2 text-3xl font-bold text-gray-900'>
{author.name}
</h1>
{author.bio && (
<p class='mb-4 text-lg text-gray-600'>
{author.bio}
</p>
)}
<div class='flex items-center space-x-4 text-sm'>
<span class='text-gray-500'>
{author.guideCount} {author.guideCount === 1 ? 'guide' : 'guides'} published
</span>
{author.socialLinks && (
<div class='flex space-x-3'>
{author.socialLinks.twitter && (
<a
href={author.socialLinks.twitter}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
Twitter
</a>
)}
{author.socialLinks.github && (
<a
href={author.socialLinks.github}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
GitHub
</a>
)}
{author.socialLinks.linkedin && (
<a
href={author.socialLinks.linkedin}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
LinkedIn
</a>
)}
{author.socialLinks.website && (
<a
href={author.socialLinks.website}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
Website
</a>
)}
</div>
)}
</div>
</div>
</div>
</div>
<!-- Author's Guides -->
<div>
<h2 class='mb-6 text-2xl font-semibold text-gray-900'>
Guides by {author.name}
</h2>
{guides.length === 0 ? (
<p class='text-gray-600'>No guides published yet.</p>
) : (
<div class='space-y-4'>
{guides.map((guide: any) => (
<article class='border-b pb-4'>
<a
href={`/guides/${guide.slug}`}
class='group block'
>
<h3 class='mb-2 text-xl font-medium text-gray-900 group-hover:text-blue-600'>
{guide.title}
{isNew(guide.publishedAt) && (
<span class='ml-2 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase'>
New
</span>
)}
</h3>
{guide.description && (
<p class='mb-2 text-gray-600'>
{guide.description}
</p>
)}
<div class='flex items-center space-x-4 text-sm text-gray-500'>
{guide.publishedAt && (
<span>
{new Date(guide.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
)}
{guide.viewCount && (
<span>{guide.viewCount.toLocaleString()} views</span>
)}
{guide.tags && guide.tags.length > 0 && (
<div class='flex flex-wrap gap-2'>
{guide.tags.slice(0, 3).map((tag: string) => (
<span class='rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600'>
{tag}
</span>
))}
</div>
)}
</div>
</a>
</article>
))}
</div>
)}
</div>
</div>
</div>
</div>
<div slot='changelog-banner'></div>
</BaseLayout>

81
src/pages/guides-v2.astro Normal file
View File

@@ -0,0 +1,81 @@
---
import SimplePageHeader from '../components/SimplePageHeader.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import { httpGet } from '../lib/http';
const apiUrl = import.meta.env.PUBLIC_API_URL || 'http://localhost:8080';
// Fetch guides from the new v2 API
const { response: guidesData, error } = await httpGet(
`${apiUrl}/v1-list-guides`,
{
perPage: 100,
sortBy: '-publishedAt',
}
);
const guides = guidesData?.data || [];
const sortedGuides = guides.sort((a: any, b: any) => {
const aDate = new Date(a.publishedAt as string);
const bDate = new Date(b.publishedAt as string);
return bDate.getTime() - aDate.getTime();
});
const getGuideType = (guide: any) => {
if (guide.roadmapId) {
return 'Roadmap Guide';
}
return 'Guide';
};
// Check if guide is within the last 15 days
const isNew = (publishedAt: string) => {
if (!publishedAt) return false;
const daysDiff = Math.floor((Date.now() - new Date(publishedAt).getTime()) / (1000 * 60 * 60 * 24));
return daysDiff < 15;
};
---
<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: any) => (
<a
class="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={`/guides/${guide.slug}`}
>
<span class="text-sm transition-transform group-hover:translate-x-2 md:text-base">
{guide.title}
{isNew(guide.publishedAt) && (
<span class="ml-2.5 rounded-xs bg-green-300 px-1.5 py-0.5 text-xs font-medium text-green-900 uppercase">
New
<span class="hidden sm:inline">
&nbsp;&middot;&nbsp;
{new Date(guide.publishedAt).toLocaleDateString('en-US', { month: 'long' })}
</span>
</span>
)}
</span>
<span class="hidden text-xs text-gray-500 capitalize sm:block">
{getGuideType(guide)}
</span>
<span class="block text-xs text-gray-400 sm:hidden"> &raquo;</span>
</a>
))}
</div>
</div>
</div>
<div slot='changelog-banner'></div>
</BaseLayout>

View File

@@ -0,0 +1,198 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { httpGet } from '../../lib/http';
export async function getStaticPaths() {
const apiUrl = import.meta.env.PUBLIC_API_URL || 'http://localhost:8080';
const { response: guidesData } = await httpGet(
`${apiUrl}/v1-list-guides`,
{
perPage: 100,
sortBy: '-publishedAt',
}
);
const guides = guidesData?.data || [];
return guides.map((guide: any) => ({
params: { guideSlug: guide.slug },
props: { guideSlug: guide.slug },
}));
}
const { guideSlug } = Astro.params;
const apiUrl = import.meta.env.PUBLIC_API_URL || 'http://localhost:8080';
const { response: guide, error } = await httpGet(
`${apiUrl}/v1-get-guide/${guideSlug}`
);
if (error || !guide) {
return Astro.redirect('/404');
}
// Convert content to HTML if needed
const renderContent = (content: any) => {
if (typeof content === 'string') {
return content;
}
// If content is JSON from tiptap editor, we'll need to convert it
// For now, return a placeholder
return '<div>Guide content will be rendered here</div>';
};
---
<BaseLayout
title={`${guide.title} - roadmap.sh`}
description={guide.description || guide.seo?.metaDescription}
permalink={`/guides/${guide.slug}`}
>
<div class='bg-white py-8'>
<div class='container'>
<div class='mx-auto max-w-4xl'>
<!-- Guide Header -->
<div class='mb-8 border-b pb-8'>
<h1 class='mb-4 text-3xl font-bold text-gray-900 sm:text-4xl'>
{guide.title}
</h1>
{guide.description && (
<p class='text-lg text-gray-600'>
{guide.description}
</p>
)}
<div class='mt-4 flex items-center space-x-4 text-sm text-gray-500'>
{guide.author && (
<a
href={`/authors/${guide.author.slug}`}
class='flex items-center hover:text-blue-600'
>
{guide.author.avatar && (
<img
src={guide.author.avatar}
alt={guide.author.name}
class='mr-2 h-6 w-6 rounded-full'
/>
)}
<span>By {guide.author.name}</span>
</a>
)}
{guide.publishedAt && (
<span>
{new Date(guide.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
)}
{guide.viewCount && (
<span>{guide.viewCount.toLocaleString()} views</span>
)}
</div>
{guide.tags && guide.tags.length > 0 && (
<div class='mt-4 flex flex-wrap gap-2'>
{guide.tags.map((tag: string) => (
<span class='rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-600'>
{tag}
</span>
))}
</div>
)}
</div>
<!-- Featured Image -->
{guide.featuredImage && (
<div class='mb-8'>
<img
src={guide.featuredImage}
alt={guide.title}
class='w-full rounded-lg'
/>
</div>
)}
<!-- Guide Content -->
<div class='prose prose-lg max-w-none'>
<Fragment set:html={renderContent(guide.content)} />
</div>
<!-- Author Bio -->
{guide.author && guide.author.bio && (
<div class='mt-12 border-t pt-8'>
<h3 class='mb-4 text-xl font-semibold'>About the Author</h3>
<div class='flex items-start space-x-4'>
{guide.author.avatar && (
<img
src={guide.author.avatar}
alt={guide.author.name}
class='h-16 w-16 rounded-full'
/>
)}
<div>
<a
href={`/authors/${guide.author.slug}`}
class='text-lg font-medium text-gray-900 hover:text-blue-600'
>
{guide.author.name}
</a>
<p class='mt-1 text-gray-600'>{guide.author.bio}</p>
{guide.author.socialLinks && (
<div class='mt-2 flex space-x-3'>
{guide.author.socialLinks.twitter && (
<a
href={guide.author.socialLinks.twitter}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
Twitter
</a>
)}
{guide.author.socialLinks.github && (
<a
href={guide.author.socialLinks.github}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
GitHub
</a>
)}
{guide.author.socialLinks.linkedin && (
<a
href={guide.author.socialLinks.linkedin}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
LinkedIn
</a>
)}
{guide.author.socialLinks.website && (
<a
href={guide.author.socialLinks.website}
target='_blank'
rel='noopener noreferrer'
class='text-gray-500 hover:text-blue-600'
>
Website
</a>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
<div slot='changelog-banner'></div>
</BaseLayout>