Compare commits

...

9 Commits

Author SHA1 Message Date
Arik Chakma
3179c95e4e feat: migrate changelogs 2024-09-23 20:16:55 +06:00
Arik Chakma
f75579806b Merge branch 'master' into feat/content-migration 2024-09-23 20:11:39 +06:00
Arik Chakma
33b617ef85 fix: migrate videos 2024-09-21 04:56:29 +06:00
Arik Chakma
2333694e2c feat: migrate projects 2024-09-21 04:26:30 +06:00
Arik Chakma
0aca915e21 fix: remove authors data 2024-09-21 03:13:27 +06:00
Arik Chakma
904ba51b7b fix: migrate content 2024-09-21 03:12:10 +06:00
Arik Chakma
8ccc8aa17d fix: remove .astro 2024-09-21 03:02:11 +06:00
Arik Chakma
ab759671d8 feat: migrate questions 2024-09-21 03:01:34 +06:00
Arik Chakma
cd453e5ad0 wip: migrate authors 2024-09-21 02:23:15 +06:00
209 changed files with 474 additions and 572 deletions

View File

@@ -1,8 +0,0 @@
{
"devToolbar": {
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1727095669945
}
}

1
.astro/types.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="astro/client" />

1
.gitignore vendored
View File

@@ -33,3 +33,4 @@ tests-examples
!/editor/readonly-editor.tsx
!/editor/renderer/renderer.ts
!/editor/renderer/index.tsx
/.astro

View File

@@ -12,8 +12,8 @@ const ALL_BEST_PRACTICE_DIR = path.join(
process.cwd(),
'/src/data/best-practices',
);
const ALL_GUIDE_DIR = path.join(process.cwd(), '/src/data/guides');
const ALl_AUTHOR_DIR = path.join(process.cwd(), '/src/data/authors');
const ALL_GUIDE_DIR = path.join(process.cwd(), '/src/content/guides');
const ALl_AUTHOR_DIR = path.join(process.cwd(), '/src/content/authors');
const ALL_ROADMAP_IMAGE_DIR = path.join(process.cwd(), '/public/roadmaps');
const ALL_BEST_PRACTICE_IMAGE_DIR = path.join(
process.cwd(),

View File

@@ -1,37 +1,39 @@
---
import type { ChangelogFileType } from '../../lib/changelog';
import { DateTime } from 'luxon';
import MarkdownFile from '../MarkdownFile.astro';
interface Props {
changelog: ChangelogFileType;
}
const { changelog } = Astro.props;
const { frontmatter } = changelog;
const { data: frontmatter } = changelog;
const { Content } = await changelog.render();
const formattedDate = DateTime.fromISO(frontmatter.date).toFormat(
const formattedDate = DateTime.fromJSDate(frontmatter.date).toFormat(
'dd LLL, yyyy',
);
---
<div class='relative'>
<span class='h-2 w-2 flex-shrink-0 rounded-full bg-gray-300 absolute top-2 -left-6'></span>
<span
class='absolute -left-6 top-2 h-2 w-2 flex-shrink-0 rounded-full bg-gray-300'
></span>
<div class='mb-3 flex items-center gap-2'>
<span class='flex-shrink-0 text-xs tracking-wide text-gray-400'>
{formattedDate}
</span>
<span class='truncate text-base font-medium'>
{changelog.frontmatter.title}
{frontmatter.title}
</span>
</div>
<div class='rounded-xl border bg-white p-6'>
<div
class='prose prose-h2:text-lg prose-h2:font-medium prose-h2:mt-3 prose-sm prose-p:mb-0 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-ul:my-0 prose-img:mt-0 prose-img:rounded-lg [&>blockquote>p]:mt-0 prose-ul:bg-gray-100 prose-ul:rounded-lg prose-ul:px-4 prose-ul:py-4 prose-ul:pl-7 [&>ul>li]:my-0 [&>ul>li]:mb-1 [&>ul]:mt-3'
class='prose prose-sm prose-h2:mt-3 prose-h2:text-lg prose-h2:font-medium prose-p:mb-0 prose-blockquote:font-normal prose-blockquote:text-gray-500 prose-ul:my-0 prose-ul:rounded-lg prose-ul:bg-gray-100 prose-ul:px-4 prose-ul:py-4 prose-ul:pl-7 prose-img:mt-0 prose-img:rounded-lg [&>blockquote>p]:mt-0 [&>ul>li]:my-0 [&>ul>li]:mb-1 [&>ul]:mt-3'
>
<changelog.Content />
<Content />
</div>
</div>
</div>

View File

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

View File

@@ -1,7 +1,7 @@
---
import type { GuideFileType } from '../lib/guide';
import GuideListItem from './GuideListItem.astro';
import { QuestionGroupType } from '../lib/question-group';
import type { QuestionGroupType } from '../lib/question-group';
export interface Props {
heading: string;
@@ -15,8 +15,8 @@ const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
...guides,
...questions,
].sort((a, b) => {
const aDate = new Date(a.frontmatter.date);
const bDate = new Date(b.frontmatter.date);
const aDate = new Date(a.data.date!);
const bDate = new Date(b.data.date!);
return bDate.getTime() - aDate.getTime();
});

View File

@@ -10,11 +10,11 @@ interface Props {
const { guide } = Astro.props;
const allHeadings = guide.getHeadings();
const { headings: allHeadings, Content } = await guide.render();
const tableOfContent = getGuideTableOfContent(allHeadings);
const showTableOfContent = tableOfContent.length > 0;
const { frontmatter: guideFrontmatter, author } = guide;
const { data: guideFrontmatter, author } = guide;
---
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
@@ -40,26 +40,26 @@ const { frontmatter: guideFrontmatter, author } = guide;
</h1>
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<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'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
alt={author.data.name}
src={author.data.imageUrl}
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
{author.data.name}
</a>
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
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'
>
Improve this Guide
</a>
</p>
<guide.Content />
<Content />
</MarkdownFile>
</div>
</article>

View File

@@ -7,7 +7,7 @@ export interface Props {
}
const { guide } = Astro.props;
const { frontmatter, author } = guide;
const { data: frontmatter, author } = guide;
return undefined;
---
@@ -18,18 +18,18 @@ return undefined;
class='hidden items-center justify-start text-gray-400 sm:flex sm:justify-center'
>
{
author?.frontmatter && (
author?.data && (
<>
<a
href={`/authors/${author.id}`}
href={`/authors/${author.slug}`}
class='inline-flex items-center font-medium hover:text-gray-600 hover:underline'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
alt={author.data.name}
src={author.data.imageUrl}
class='mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
{author.data.name}
</a>
<span class='mx-1.5'>&middot;</span>
</>
@@ -39,7 +39,7 @@ return undefined;
<span class='mx-1.5'>&middot;</span>
<a
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
>
</p>

View File

@@ -1,7 +1,7 @@
---
import type { GuideFileType, GuideFrontmatter } from '../lib/guide';
import type { GuideFileType } from '../lib/guide';
import { replaceVariables } from '../lib/markdown';
import { QuestionGroupType } from '../lib/question-group';
import type { QuestionGroupType } from '../lib/question-group';
export interface Props {
guide: GuideFileType | QuestionGroupType;
@@ -14,7 +14,7 @@ function isQuestionGroupType(
}
const { guide } = Astro.props;
const { frontmatter, id } = guide;
const { data: frontmatter, slug: id } = guide;
let pageUrl = '';
let guideType = '';
@@ -23,9 +23,9 @@ if (isQuestionGroupType(guide)) {
pageUrl = `/questions/${id}`;
guideType = 'Questions';
} else {
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
const excludedBySlug = (frontmatter as GuideFileType['data']).excludedBySlug;
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
guideType = (frontmatter as GuideFrontmatter).type;
guideType = (frontmatter as GuideFileType['data']).type;
}
---
@@ -46,7 +46,7 @@ if (isQuestionGroupType(guide)) {
New
<span class='hidden sm:inline'>
&middot;
{new Date(frontmatter.date).toLocaleString('default', {
{new Date(frontmatter.date!).toLocaleString('default', {
month: 'long',
})}
</span>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,9 @@
---
import {
getGuideTableOfContent,
type GuideFileType,
HeadingGroupType,
} from '../../lib/guide';
import { getGuideTableOfContent, type HeadingGroupType } from '../../lib/guide';
import MarkdownFile from '../MarkdownFile.astro';
import { TableOfContent } from '../TableOfContent/TableOfContent';
import { markdownToHtml, replaceVariables } from '../../lib/markdown';
import { QuestionGroupType } from '../../lib/question-group';
import type { QuestionGroupType } from '../../lib/question-group';
import { QuestionsList } from './QuestionsList';
interface Props {
@@ -16,19 +12,17 @@ interface Props {
const { questionGroup } = Astro.props;
const allHeadings = questionGroup.getHeadings();
const { headings: allHeadings, Content } = await questionGroup.render();
const tableOfContent: HeadingGroupType[] = [
...getGuideTableOfContent(allHeadings),
{
depth: 2,
title: 'Test with Flashcards',
text: 'Test yourself with Flashcards',
children: [],
slug: 'test-with-flashcards',
text: 'Test yourself with Flashcards',
},
{
depth: 2,
title: 'Questions List',
children: [
{
depth: 2,
@@ -58,7 +52,7 @@ const tableOfContent: HeadingGroupType[] = [
];
const showTableOfContent = tableOfContent.length > 0;
const { frontmatter: guideFrontmatter, author } = questionGroup;
const { data: guideFrontmatter, author } = questionGroup;
---
<article class='lg:grid lg:max-w-full lg:grid-cols-[1fr_minmax(0,700px)_1fr]'>
@@ -86,20 +80,20 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
author && (
<p class='my-0 flex items-center justify-start text-sm text-gray-400'>
<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'
>
<img
alt={author.frontmatter.name}
src={author.frontmatter.imageUrl}
alt={author.data.name}
src={author.data.imageUrl}
class='mb-0 mr-2 inline h-5 w-5 rounded-full'
/>
{author.frontmatter.name}
{author.data.name}
</a>
<span class='mx-2 hidden sm:inline'>&middot;</span>
<a
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'
>
Improve this Guide
@@ -107,7 +101,7 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
</p>
)
}
<questionGroup.Content />
<Content />
<h2 id='test-with-flashcards'>Test yourself with Flashcards</h2>
<p>
@@ -116,7 +110,7 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
</p>
<div class='mx-0 sm:-mb-32'>
<QuestionsList
groupId={questionGroup.id}
groupId={questionGroup.slug}
questions={questionGroup.questions}
client:load
/>
@@ -136,8 +130,8 @@ const { frontmatter: guideFrontmatter, author } = questionGroup;
</h3>
{questionGroup.questions
.filter((q) => {
return q.topics
.map((t) => t.toLowerCase())
return q?.topics
?.map((t) => t.toLowerCase())
.includes(questionLevel);
})
.map((q) => (

View File

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

View File

@@ -6,21 +6,21 @@ export interface Props {
}
const { video } = Astro.props;
const { frontmatter, id } = video;
const { data: frontmatter, slug: id } = video;
---
<a
class:list={[
'block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b',
'text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600',
]}
href={`/videos/${id}`}
>
<span class='group-hover:translate-x-2 transition-transform'>
<span class='transition-transform group-hover:translate-x-2'>
{frontmatter.title}
{
frontmatter.isNew && (
<span class='bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5'>
<span class='ml-1.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900'>
New
<span class='hidden sm:inline'>
&middot;
@@ -32,9 +32,9 @@ const { frontmatter, id } = video;
)
}
</span>
<span class='capitalize text-gray-500 text-xs hidden sm:block'>
<span class='hidden text-xs capitalize text-gray-500 sm:block'>
{frontmatter.duration}
</span>
<span class='text-gray-400 text-xs block sm:hidden'> &raquo;</span>
<span class='block text-xs text-gray-400 sm:hidden'> &raquo;</span>
</a>

23
src/content/author.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineCollection, z } from 'astro:content';
export const authorCollection = defineCollection({
type: 'content',
schema: z.object({
name: z.string(),
imageUrl: z.string(),
employment: z
.object({
title: z.string(),
company: z.string(),
})
.optional(),
social: z
.object({
linkedin: z.string().optional(),
twitter: z.string().optional(),
github: z.string().optional(),
website: z.string().optional(),
})
.optional(),
}),
});

16
src/content/changelog.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineCollection, z } from 'astro:content';
export const changelogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
seo: z
.object({
title: z.string(),
description: z.string().optional(),
})
.optional(),
date: z.date(),
}),
});

15
src/content/config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { authorCollection } from './author';
import { changelogCollection } from './changelog';
import { guideCollection } from './guide';
import { projectCollection } from './project';
import { questionGroupCollection } from './question-group';
import { videoCollection } from './video';
export const collections = {
authors: authorCollection,
guides: guideCollection,
'question-groups': questionGroupCollection,
projects: projectCollection,
videos: videoCollection,
changelogs: changelogCollection,
};

25
src/content/guide.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineCollection, z } from 'astro:content';
export const guideCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
authorId: z.string(),
canonicalUrl: z.string().optional(),
excludedBySlug: z.string().optional(),
seo: z.object({
title: z.string(),
description: z.string(),
ogImageUrl: z.string().optional(),
}),
isNew: z.boolean(),
type: z.enum(['visual', 'textual']),
date: z.date(),
sitemap: z.object({
priority: z.number(),
changefreq: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
}),
tags: z.array(z.string()).optional(),
}),
});

28
src/content/project.ts Normal file
View File

@@ -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()),
}),
});

Some files were not shown because too many files have changed in this diff Show More