mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-30 04:30:01 +02:00
Add guides v2
This commit is contained in:
194
src/pages/authors/[authorSlug].astro
Normal file
194
src/pages/authors/[authorSlug].astro
Normal 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
81
src/pages/guides-v2.astro
Normal 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">
|
||||
·
|
||||
{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"> »</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot='changelog-banner'></div>
|
||||
</BaseLayout>
|
198
src/pages/guides/[guideSlug].astro
Normal file
198
src/pages/guides/[guideSlug].astro
Normal 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>
|
Reference in New Issue
Block a user