feat: migrate projects

feat/collection
Arik Chakma 2 months ago
parent 0aca915e21
commit 2333694e2c
  1. 3
      src/components/CustomRoadmap/FlowRoadmapRenderer.tsx
  2. 4
      src/components/Guide/GuideContent.astro
  3. 14
      src/components/GuideHeader.astro
  4. 5
      src/components/Projects/ListProjectSolutions.tsx
  5. 8
      src/components/Projects/ProjectCard.tsx
  6. 29
      src/components/Projects/ProjectsList.tsx
  7. 16
      src/components/Projects/ProjectsPage.tsx
  8. 6
      src/components/Questions/QuestionGuide.astro
  9. 9
      src/components/VideoHeader.astro
  10. 2
      src/content/config.ts
  11. 28
      src/content/project.ts
  12. 0
      src/content/projects/accessible-form-ui.md
  13. 0
      src/content/projects/accordion.md
  14. 0
      src/content/projects/basic-dockerfile.md
  15. 0
      src/content/projects/basic-html-website.md
  16. 0
      src/content/projects/blogging-platform-api.md
  17. 0
      src/content/projects/broadcast-server.md
  18. 0
      src/content/projects/caching-server.md
  19. 0
      src/content/projects/changelog-component.md
  20. 0
      src/content/projects/cookie-consent.md
  21. 0
      src/content/projects/custom-dropdown.md
  22. 0
      src/content/projects/database-backup-utility.md
  23. 0
      src/content/projects/datepicker-ui.md
  24. 0
      src/content/projects/ecommerce-api.md
  25. 0
      src/content/projects/expense-tracker-api.md
  26. 0
      src/content/projects/expense-tracker.md
  27. 0
      src/content/projects/fitness-workout-tracker.md
  28. 0
      src/content/projects/github-random-repo.md
  29. 0
      src/content/projects/github-user-activity.md
  30. 0
      src/content/projects/image-grid.md
  31. 0
      src/content/projects/image-processing-service.md
  32. 0
      src/content/projects/log-archive-tool.md
  33. 0
      src/content/projects/markdown-note-taking-app.md
  34. 0
      src/content/projects/movie-reservation-system.md
  35. 0
      src/content/projects/number-guessing-game.md
  36. 0
      src/content/projects/personal-blog.md
  37. 0
      src/content/projects/portfolio-website.md
  38. 0
      src/content/projects/realtime-leaderboard-system.md
  39. 0
      src/content/projects/reddit-client.md
  40. 0
      src/content/projects/restricted-textarea.md
  41. 0
      src/content/projects/scalable-ecommerce-platform.md
  42. 0
      src/content/projects/simple-tabs.md
  43. 0
      src/content/projects/single-page-cv.md
  44. 0
      src/content/projects/task-tracker-js.md
  45. 0
      src/content/projects/task-tracker.md
  46. 6
      src/content/projects/temperature-converter.md
  47. 0
      src/content/projects/testimonial-cards.md
  48. 0
      src/content/projects/todo-list-api.md
  49. 0
      src/content/projects/tooltip-ui.md
  50. 0
      src/content/projects/unit-converter.md
  51. 0
      src/content/projects/url-shortening-service.md
  52. 0
      src/content/projects/weather-api-wrapper-service.md
  53. 68
      src/lib/project.ts
  54. 4
      src/lib/video.ts
  55. 2
      src/pages/[roadmapId]/projects.astro
  56. 2
      src/pages/authors/[authorId].astro
  57. 2
      src/pages/index.astro
  58. 16
      src/pages/pages.json.ts
  59. 19
      src/pages/projects/[projectId]/index.astro
  60. 10
      src/pages/projects/[projectId]/solutions.astro
  61. 4
      src/pages/projects/index.astro
  62. 2
      src/pages/questions/[questionGroupId].astro

@ -159,7 +159,8 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
{hideRenderer && ( {hideRenderer && (
<EmptyRoadmap <EmptyRoadmap
roadmapId={roadmapId} roadmapId={roadmapId}
canManage={roadmap.canManage} // @ts-ignore
canManage={roadmap?.canManage}
className="grow" className="grow"
/> />
)} )}

@ -40,7 +40,7 @@ const { data: guideFrontmatter, author } = guide;
</h1> </h1>
<p class='my-0 flex items-center justify-start text-sm text-gray-400'> <p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a <a
href={`/authors/${author.id}`} href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline' class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
> >
<img <img
@ -53,7 +53,7 @@ const { data: guideFrontmatter, author } = guide;
<span class='mx-2 hidden sm:inline'>&middot;</span> <span class='mx-2 hidden sm:inline'>&middot;</span>
<a <a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline' class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`} href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.slug}.md`}
target='_blank' target='_blank'
> >
Improve this Guide Improve this Guide

@ -7,7 +7,7 @@ export interface Props {
} }
const { guide } = Astro.props; const { guide } = Astro.props;
const { frontmatter, author } = guide; const { data: frontmatter, author } = guide;
return undefined; return undefined;
--- ---
@ -18,18 +18,18 @@ return undefined;
class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center' class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center'
> >
{ {
author?.frontmatter && ( author?.data && (
<> <>
<a <a
href={`/authors/${author.id}`} href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline' class='inline-flex items-center font-medium hover:text-gray-600 hover:underline'
> >
<img <img
alt={author.frontmatter.name} alt={author.data.name}
src={author.frontmatter.imageUrl} src={author.data.imageUrl}
class='mr-2 inline h-5 w-5 rounded-full' class='mr-2 inline h-5 w-5 rounded-full'
/> />
{author.frontmatter.name} {author.data.name}
</a> </a>
<span class='mx-1.5'>&middot;</span> <span class='mx-1.5'>&middot;</span>
</> </>
@ -39,7 +39,7 @@ return undefined;
<span class='mx-1.5'>&middot;</span> <span class='mx-1.5'>&middot;</span>
<a <a
class='text-blue-400 hover:text-blue-500 hover:underline' class='text-blue-400 hover:text-blue-500 hover:underline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.id}.md`} href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/guides/${guide.slug}.md`}
target='_blank'>Improve this Guide</a target='_blank'>Improve this Guide</a
> >
</p> </p>

@ -14,8 +14,7 @@ import { showLoginPopup } from '../../lib/popup';
import { VoteButton } from './VoteButton.tsx'; import { VoteButton } from './VoteButton.tsx';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { SelectLanguages } from './SelectLanguages.tsx'; import { SelectLanguages } from './SelectLanguages.tsx';
import type { ProjectFrontmatter } from '../../lib/project.ts'; import type { ProjectFileType } from '../../lib/project.ts';
import { ProjectSolutionModal } from './ProjectSolutionModal.tsx';
export interface ProjectStatusDocument { export interface ProjectStatusDocument {
_id?: string; _id?: string;
@ -65,7 +64,7 @@ type PageState = {
}; };
type ListProjectSolutionsProps = { type ListProjectSolutionsProps = {
project: ProjectFrontmatter; project: ProjectFileType['data'];
projectId: string; projectId: string;
}; };

@ -1,10 +1,8 @@
import { Badge } from '../Badge.tsx'; import { Badge } from '../Badge.tsx';
import type { import type { ProjectFileType } from '../../lib/project.ts';
ProjectDifficultyType,
ProjectFileType,
} from '../../lib/project.ts';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { formatCommaNumber } from '../../lib/number.ts'; import { formatCommaNumber } from '../../lib/number.ts';
import type { ProjectDifficultyType } from '../../content/project.ts';
type ProjectCardProps = { type ProjectCardProps = {
project: ProjectFileType; project: ProjectFileType;
@ -20,7 +18,7 @@ const badgeVariants: Record<ProjectDifficultyType, string> = {
export function ProjectCard(props: ProjectCardProps) { export function ProjectCard(props: ProjectCardProps) {
const { project, userCount = 0 } = props; const { project, userCount = 0 } = props;
const { frontmatter, id } = project; const { data: frontmatter, slug: id } = project;
return ( return (
<a <a

@ -2,16 +2,16 @@ import { ProjectCard } from './ProjectCard.tsx';
import { HeartHandshake, Trash2 } from 'lucide-react'; import { HeartHandshake, Trash2 } from 'lucide-react';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { import { type ProjectFileType } from '../../lib/project.ts';
projectDifficulties,
type ProjectDifficultyType,
type ProjectFileType,
} from '../../lib/project.ts';
import { import {
deleteUrlParam, deleteUrlParam,
getUrlParams, getUrlParams,
setUrlParams, setUrlParams,
} from '../../lib/browser.ts'; } from '../../lib/browser.ts';
import {
projectDifficulties,
type ProjectDifficultyType,
} from '../../content/project.ts';
type DifficultyButtonProps = { type DifficultyButtonProps = {
difficulty: ProjectDifficultyType; difficulty: ProjectDifficultyType;
@ -56,7 +56,7 @@ export function ProjectsList(props: ProjectsListProps) {
const result = new Map<ProjectDifficultyType, ProjectFileType[]>(); const result = new Map<ProjectDifficultyType, ProjectFileType[]>();
for (const project of projects) { for (const project of projects) {
const difficulty = project.frontmatter.difficulty; const difficulty = project.data.difficulty;
if (!result.has(difficulty)) { if (!result.has(difficulty)) {
result.set(difficulty, []); result.set(difficulty, []);
@ -78,6 +78,7 @@ export function ProjectsList(props: ProjectsListProps) {
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{projectDifficulties.map((projectDifficulty) => ( {projectDifficulties.map((projectDifficulty) => (
<DifficultyButton <DifficultyButton
key={projectDifficulty}
onClick={() => { onClick={() => {
setDifficulty(projectDifficulty); setDifficulty(projectDifficulty);
setUrlParams({ difficulty: projectDifficulty }); setUrlParams({ difficulty: projectDifficulty });
@ -119,18 +120,24 @@ export function ProjectsList(props: ProjectsListProps) {
{matchingProjects {matchingProjects
.sort((project) => { .sort((project) => {
return project.frontmatter.difficulty === 'beginner' return project.data.difficulty === 'beginner'
? -1 ? -1
: project.frontmatter.difficulty === 'intermediate' : project.data.difficulty === 'intermediate'
? 0 ? 0
: 1; : 1;
}) })
.sort((a, b) => { .sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort; return a.data.sort - b.data.sort;
}) })
.map((matchingProject) => { .map((matchingProject) => {
const count = userCounts[matchingProject?.id] || 0; const count = userCounts[matchingProject?.slug] || 0;
return <ProjectCard project={matchingProject} userCount={count} />; return (
<ProjectCard
key={matchingProject.slug}
project={matchingProject}
userCount={count}
/>
);
})} })}
</div> </div>
</div> </div>

@ -7,11 +7,9 @@ import {
setUrlParams, setUrlParams,
} from '../../lib/browser.ts'; } from '../../lib/browser.ts';
import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx'; import { CategoryFilterButton } from '../Roadmaps/CategoryFilterButton.tsx';
import { import { type ProjectFileType } from '../../lib/project.ts';
projectDifficulties,
type ProjectFileType,
} from '../../lib/project.ts';
import { ProjectCard } from './ProjectCard.tsx'; import { ProjectCard } from './ProjectCard.tsx';
import { projectDifficulties } from '../../content/project.ts';
type ProjectGroup = { type ProjectGroup = {
id: string; id: string;
@ -28,7 +26,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
const { roadmapsProjects, userCounts } = props; const { roadmapsProjects, userCounts } = props;
const allUniqueProjectIds = new Set<string>( const allUniqueProjectIds = new Set<string>(
roadmapsProjects.flatMap((group) => roadmapsProjects.flatMap((group) =>
group.projects.map((project) => project.id), group.projects.map((project) => project.slug),
), ),
); );
const allUniqueProjects = useMemo( const allUniqueProjects = useMemo(
@ -37,7 +35,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
.map((id) => .map((id) =>
roadmapsProjects roadmapsProjects
.flatMap((group) => group.projects) .flatMap((group) => group.projects)
.find((project) => project.id === id), .find((project) => project.slug === id),
) )
.filter(Boolean) as ProjectFileType[], .filter(Boolean) as ProjectFileType[],
[allUniqueProjectIds], [allUniqueProjectIds],
@ -67,8 +65,8 @@ export function ProjectsPage(props: ProjectsPageProps) {
const sortedVisibleProjects = useMemo( const sortedVisibleProjects = useMemo(
() => () =>
visibleProjects.sort((a, b) => { visibleProjects.sort((a, b) => {
const projectADifficulty = a?.frontmatter.difficulty || 'beginner'; const projectADifficulty = a?.data.difficulty || 'beginner';
const projectBDifficulty = b?.frontmatter.difficulty || 'beginner'; const projectBDifficulty = b?.data.difficulty || 'beginner';
return ( return (
projectDifficulties.indexOf(projectADifficulty) - projectDifficulties.indexOf(projectADifficulty) -
projectDifficulties.indexOf(projectBDifficulty) projectDifficulties.indexOf(projectBDifficulty)
@ -189,7 +187,7 @@ export function ProjectsPage(props: ProjectsPageProps) {
<ProjectCard <ProjectCard
key={project.id} key={project.id}
project={project} project={project}
userCount={userCounts[project.id] || 0} userCount={userCounts[project.slug] || 0}
/> />
))} ))}
</div> </div>

@ -80,7 +80,7 @@ const { data: guideFrontmatter, author } = questionGroup;
author && ( author && (
<p class='my-0 flex items-center justify-start text-sm text-gray-400'> <p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<a <a
href={`/authors/${author?.id}`} href={`/authors/${author?.slug}`}
class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline' class='inline-flex items-center font-medium underline-offset-2 hover:text-gray-600 hover:underline'
> >
<img <img
@ -93,7 +93,7 @@ const { data: guideFrontmatter, author } = questionGroup;
<span class='mx-2 hidden sm:inline'>&middot;</span> <span class='mx-2 hidden sm:inline'>&middot;</span>
<a <a
class='hidden underline-offset-2 hover:text-gray-600 sm:inline' class='hidden underline-offset-2 hover:text-gray-600 sm:inline'
href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.id}`} href={`https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/question-groups/${questionGroup.slug}`}
target='_blank' target='_blank'
> >
Improve this Guide Improve this Guide
@ -110,7 +110,7 @@ const { data: guideFrontmatter, author } = questionGroup;
</p> </p>
<div class='mx-0 sm:-mb-32'> <div class='mx-0 sm:-mb-32'>
<QuestionsList <QuestionsList
groupId={questionGroup.id} groupId={questionGroup.slug}
questions={questionGroup.questions} questions={questionGroup.questions}
client:load client:load
/> />

@ -1,6 +1,5 @@
--- ---
import type { VideoFileType } from '../lib/video'; import type { VideoFileType } from '../lib/video';
import YouTubeAlert from './YouTubeAlert.astro';
export interface Props { export interface Props {
video: VideoFileType; video: VideoFileType;
@ -16,15 +15,15 @@ const { frontmatter, author } = video;
class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center' class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center'
> >
<a <a
href={`/authors/${author.id}`} href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline' class='inline-flex items-center font-medium hover:text-gray-600 hover:underline'
> >
<img <img
alt={author.frontmatter.name} alt={author.data.name}
src={author.frontmatter.imageUrl} src={author.data.imageUrl}
class='mr-2 inline h-5 w-5 rounded-full' class='mr-2 inline h-5 w-5 rounded-full'
/> />
{author.frontmatter.name} {author.data.name}
</a> </a>
<span class='mx-1.5'>&middot;</span> <span class='mx-1.5'>&middot;</span>
<span class='capitalize'>Illustrated Video</span> <span class='capitalize'>Illustrated Video</span>

@ -1,9 +1,11 @@
import { authorCollection } from './author'; import { authorCollection } from './author';
import { guideCollection } from './guide'; import { guideCollection } from './guide';
import { projectCollection } from './project';
import { questionGroupCollection } from './question-group'; import { questionGroupCollection } from './question-group';
export const collections = { export const collections = {
authors: authorCollection, authors: authorCollection,
guides: guideCollection, guides: guideCollection,
'question-groups': questionGroupCollection, 'question-groups': questionGroupCollection,
projects: projectCollection,
}; };

@ -0,0 +1,28 @@
import { defineCollection, z } from 'astro:content';
export const projectDifficulties = [
'beginner',
'intermediate',
'advanced',
] as const;
export type ProjectDifficultyType = (typeof projectDifficulties)[number];
export const projectCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
isNew: z.boolean(),
sort: z.number(),
difficulty: z.enum(projectDifficulties),
nature: z.string(),
skills: z.array(z.string()),
seo: z.object({
title: z.string(),
description: z.string(),
keywords: z.array(z.string()),
ogImageUrl: z.string().optional(),
}),
roadmapIds: z.array(z.string()),
}),
});

@ -11,9 +11,9 @@ skills:
- JavaScript - JavaScript
- DOM Manipulation - DOM Manipulation
seo: seo:
- title: Build a Temperature Converter with JavaScript title: Build a Temperature Converter with JavaScript
- description: Learn how to create an interactive temperature converter that converts between Celsius, Fahrenheit, and Kelvin using JavaScript. description: Learn how to create an interactive temperature converter that converts between Celsius, Fahrenheit, and Kelvin using JavaScript.
- keywords: keywords:
- 'temperature converter' - 'temperature converter'
- 'javascript project' - 'javascript project'
- 'unit conversion' - 'unit conversion'

@ -1,54 +1,17 @@
import type { MarkdownFileType } from './file'; import { getCollection, type CollectionEntry } from 'astro:content';
import { getRoadmapById, type RoadmapFileType } from './roadmap.ts'; import { getRoadmapById, type RoadmapFileType } from './roadmap.ts';
export const projectDifficulties = [ export type ProjectFileType = CollectionEntry<'projects'> & {
'beginner',
'intermediate',
'advanced',
] as const;
export type ProjectDifficultyType = (typeof projectDifficulties)[number];
export interface ProjectFrontmatter {
title: string;
description: string;
isNew: boolean;
sort: number;
difficulty: ProjectDifficultyType;
nature: string;
skills: string[];
seo: {
title: string;
description: string;
keywords: string[];
ogImageUrl: string;
};
roadmapIds: string[];
}
export type ProjectFileType = MarkdownFileType<ProjectFrontmatter> & {
id: string;
roadmaps: RoadmapFileType[]; roadmaps: RoadmapFileType[];
}; };
/**
* Generates id from the given project file
* @param filePath Markdown file path
*
* @returns unique project identifier
*/
function projectPathToId(filePath: string): string {
const fileName = filePath.split('/').pop() || '';
return fileName.replace('.md', '');
}
export async function getProjectsByRoadmapId( export async function getProjectsByRoadmapId(
roadmapId: string, roadmapId: string,
): Promise<ProjectFileType[]> { ): Promise<ProjectFileType[]> {
const projects = await getAllProjects(); const projects = await getAllProjects();
return projects.filter((project) => return projects.filter((project) =>
project.frontmatter?.roadmapIds?.includes(roadmapId), project.data?.roadmapIds?.includes(roadmapId),
); );
} }
@ -63,26 +26,20 @@ export async function getAllProjects(): Promise<ProjectFileType[]> {
return tempProjects; return tempProjects;
} }
const projects = import.meta.glob<ProjectFileType>( tempProjects = await getCollection('projects');
'/src/data/projects/*.md',
{
eager: true,
},
);
tempProjects = Object.values(projects).map((projectFile) => ({
...projectFile,
id: projectPathToId(projectFile.file),
}));
return tempProjects; return tempProjects;
} }
export async function getProjectById( export async function getProjectById(
groupId: string, groupId: string,
): Promise<ProjectFileType> { ): Promise<ProjectFileType> {
const project = await import(`../data/projects/${groupId}.md`); const projects = await getAllProjects();
const roadmapIds = project.frontmatter.roadmapIds || []; const project = projects.find((project) => project.slug === groupId);
if (!project) {
throw new Error(`Project not found with id: ${groupId}`);
}
const roadmapIds = project.data.roadmapIds || [];
const roadmaps = await Promise.all( const roadmaps = await Promise.all(
roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)), roadmapIds.map((roadmapId: string) => getRoadmapById(roadmapId)),
); );
@ -90,7 +47,6 @@ export async function getProjectById(
return { return {
...project, ...project,
roadmaps: roadmaps, roadmaps: roadmaps,
id: projectPathToId(project.file),
}; };
} }
@ -101,7 +57,7 @@ export async function getRoadmapsProjects(): Promise<
const roadmapsProjects: Record<string, ProjectFileType[]> = {}; const roadmapsProjects: Record<string, ProjectFileType[]> = {};
projects.forEach((project) => { projects.forEach((project) => {
project.frontmatter.roadmapIds.forEach((roadmapId) => { project.data.roadmapIds.forEach((roadmapId) => {
if (!roadmapsProjects[roadmapId]) { if (!roadmapsProjects[roadmapId]) {
roadmapsProjects[roadmapId] = []; roadmapsProjects[roadmapId] = [];
} }

@ -44,7 +44,7 @@ export async function getVideosByAuthor(
): Promise<VideoFileType[]> { ): Promise<VideoFileType[]> {
const allVideos = await getAllVideos(); const allVideos = await getAllVideos();
return allVideos.filter((video) => video.author?.id === authorId); return allVideos.filter((video) => video.author?.slug === authorId);
} }
/** /**
@ -63,7 +63,7 @@ export async function getAllVideos(): Promise<VideoFileType[]> {
...videoFile, ...videoFile,
id: videoPathToId(videoFile.file), id: videoPathToId(videoFile.file),
author: allAuthors.find( author: allAuthors.find(
(author) => author.id === videoFile.frontmatter.authorId, (author) => author.slug === videoFile.frontmatter.authorId,
)!, )!,
})); }));

@ -52,7 +52,7 @@ const nounTitle =
const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`; const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`;
const projects = await getProjectsByRoadmapId(roadmapId); const projects = await getProjectsByRoadmapId(roadmapId);
const projectIds = projects.map((project) => project.id); const projectIds = projects.map((project) => project.slug);
const projectApiClient = projectApi(Astro); const projectApiClient = projectApi(Astro);
const { response: userCounts } = const { response: userCounts } =

@ -32,7 +32,7 @@ const videos = await getVideosByAuthor(authorId);
--- ---
<BaseLayout <BaseLayout
permalink={`/authors/${author.id}`} permalink={`/authors/${author.slug}`}
title={`${authorFrontmatter.name} - Author at roadmap.sh`} title={`${authorFrontmatter.name} - Author at roadmap.sh`}
briefTitle={authorFrontmatter.name} briefTitle={authorFrontmatter.name}
ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`} ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`}

@ -93,7 +93,7 @@ const videos = await getAllVideos();
allowBookmark={false} allowBookmark={false}
featuredItems={questionGroups.map((questionGroup) => ({ featuredItems={questionGroups.map((questionGroup) => ({
text: questionGroup.data.briefTitle, text: questionGroup.data.briefTitle,
url: `/questions/${questionGroup.id}`, url: `/questions/${questionGroup.slug}`,
isNew: questionGroup.data.isNew, isNew: questionGroup.data.isNew,
}))} }))}
/> />

@ -34,16 +34,16 @@ export async function GET() {
group: 'Best Practices', group: 'Best Practices',
})), })),
...questionGroups.map((questionGroup) => ({ ...questionGroups.map((questionGroup) => ({
id: questionGroup.id, id: questionGroup.slug,
url: `/questions/${questionGroup.id}`, url: `/questions/${questionGroup.slug}`,
title: questionGroup.data.briefTitle, title: questionGroup.data.briefTitle,
group: 'Questions', group: 'Questions',
})), })),
...guides.map((guide) => ({ ...guides.map((guide) => ({
id: guide.id, id: guide.slug,
url: guide.data.excludedBySlug url: guide.data.excludedBySlug
? guide.data.excludedBySlug ? guide.data.excludedBySlug
: `/guides/${guide.id}`, : `/guides/${guide.slug}`,
title: guide.data.title, title: guide.data.title,
description: guide.data.description, description: guide.data.description,
authorId: guide.data.authorId, authorId: guide.data.authorId,
@ -56,10 +56,10 @@ export async function GET() {
group: 'Videos', group: 'Videos',
})), })),
...projects.map((project) => ({ ...projects.map((project) => ({
id: project.id, id: project.slug,
url: `/projects/${project.id}`, url: `/projects/${project.slug}`,
title: project.frontmatter.title, title: project.data.title,
description: project.frontmatter.description, description: project.data.description,
group: 'Projects', group: 'Projects',
})), })),
]), ]),

@ -1,11 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { Badge } from '../../../components/Badge'; import { Badge } from '../../../components/Badge';
import { import { getAllProjects, getProjectById } from '../../../lib/project';
getAllProjects,
getProjectById,
type ProjectFrontmatter,
} from '../../../lib/project';
import AstroIcon from '../../../components/AstroIcon.astro'; import AstroIcon from '../../../components/AstroIcon.astro';
import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper'; import { ProjectStepper } from '../../../components/Projects/StatusStepper/ProjectStepper';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
@ -14,7 +10,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects(); const projects = await getAllProjects();
return projects return projects
.map((project) => project.id) .map((project) => project.slug)
.map((projectId) => ({ .map((projectId) => ({
params: { projectId }, params: { projectId },
})); }));
@ -27,7 +23,8 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params; const { projectId } = Astro.params as Params;
const project = await getProjectById(projectId); const project = await getProjectById(projectId);
const projectData = project.frontmatter as ProjectFrontmatter; const projectData = project.data;
const { Content } = await project.render();
let jsonLdSchema: any[] = []; let jsonLdSchema: any[] = [];
@ -49,7 +46,11 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
> >
<div class='bg-gray-50'> <div class='bg-gray-50'>
<div class='container'> <div class='container'>
<ProjectTabs parentRoadmapId={parentRoadmapId} projectId={projectId} activeTab='details' /> <ProjectTabs
parentRoadmapId={parentRoadmapId}
projectId={projectId}
activeTab='details'
/>
<div <div
class='mb-4 rounded-lg border bg-gradient-to-b from-gray-100 to-white to-10% p-4 py-2 sm:p-5' class='mb-4 rounded-lg border bg-gradient-to-b from-gray-100 to-white to-10% p-4 py-2 sm:p-5'
@ -80,7 +81,7 @@ const parentRoadmapId = projectData?.roadmapIds?.[0] || '';
<div <div
class='prose max-w-full prose-h2:mb-3 prose-h2:mt-5 prose-h3:mb-1 prose-h3:mt-5 prose-p:mb-2 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-pre:my-3 prose-ul:my-3.5 prose-hr:my-5 [&>ul>li]:my-1' class='prose max-w-full prose-h2:mb-3 prose-h2:mt-5 prose-h3:mb-1 prose-h3:mt-5 prose-p:mb-2 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-pre:my-3 prose-ul:my-3.5 prose-hr:my-5 [&>ul>li]:my-1'
> >
<project.Content /> <Content />
</div> </div>
<div <div

@ -1,10 +1,6 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { import { getAllProjects, getProjectById } from '../../../lib/project';
getAllProjects,
getProjectById,
type ProjectFrontmatter,
} from '../../../lib/project';
import { ProjectTabs } from '../../../components/Projects/ProjectTabs'; import { ProjectTabs } from '../../../components/Projects/ProjectTabs';
import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions'; import { ListProjectSolutions } from '../../../components/Projects/ListProjectSolutions';
import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal'; import { ProjectSolutionModal } from '../../../components/Projects/ProjectSolutionModal';
@ -13,7 +9,7 @@ export async function getStaticPaths() {
const projects = await getAllProjects(); const projects = await getAllProjects();
return projects return projects
.map((project) => project.id) .map((project) => project.slug)
.map((projectId) => ({ .map((projectId) => ({
params: { projectId }, params: { projectId },
})); }));
@ -26,7 +22,7 @@ interface Params extends Record<string, string | undefined> {
const { projectId } = Astro.params as Params; const { projectId } = Astro.params as Params;
const project = await getProjectById(projectId); const project = await getProjectById(projectId);
const projectData = project.frontmatter as ProjectFrontmatter; const projectData = project.data;
let jsonLdSchema: any[] = []; let jsonLdSchema: any[] = [];

@ -12,7 +12,7 @@ const allRoadmapIds = Object.keys(roadmapProjects);
const allRoadmaps = await getRoadmapsByIds(allRoadmapIds); const allRoadmaps = await getRoadmapsByIds(allRoadmapIds);
const enrichedRoadmaps = allRoadmaps.map((roadmap) => { const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projects = (roadmapProjects[roadmap.id] || []).sort((a, b) => { const projects = (roadmapProjects[roadmap.id] || []).sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort; return a.data.sort - b.data.sort;
}); });
return { return {
@ -25,7 +25,7 @@ const enrichedRoadmaps = allRoadmaps.map((roadmap) => {
const projectIds = allRoadmapIds const projectIds = allRoadmapIds
.map((id) => roadmapProjects[id]) .map((id) => roadmapProjects[id])
.flat() .flat()
.map((project) => project.id); .map((project) => project.slug);
const projectApiClient = projectApi(Astro); const projectApiClient = projectApi(Astro);
const { response: userCounts } = const { response: userCounts } =
await projectApiClient.listProjectsUserCount(projectIds); await projectApiClient.listProjectsUserCount(projectIds);

@ -34,7 +34,7 @@ const { data: frontmatter } = questionGroup;
briefTitle={frontmatter.briefTitle} briefTitle={frontmatter.briefTitle}
description={frontmatter.seo.description} description={frontmatter.seo.description}
keywords={frontmatter.seo.keywords} keywords={frontmatter.seo.keywords}
permalink={`/questions/${questionGroup.id}`} permalink={`/questions/${questionGroup.slug}`}
> >
{ {
!frontmatter.authorId && ( !frontmatter.authorId && (

Loading…
Cancel
Save