mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2026-03-13 10:11:55 +08:00
Compare commits
9 Commits
master
...
feat/colle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3179c95e4e | ||
|
|
f75579806b | ||
|
|
33b617ef85 | ||
|
|
2333694e2c | ||
|
|
0aca915e21 | ||
|
|
904ba51b7b | ||
|
|
8ccc8aa17d | ||
|
|
ab759671d8 | ||
|
|
cd453e5ad0 |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"devToolbar": {
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1727095669945
|
||||
}
|
||||
}
|
||||
1
.astro/types.d.ts
vendored
1
.astro/types.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="astro/client" />
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ tests-examples
|
||||
!/editor/readonly-editor.tsx
|
||||
!/editor/renderer/renderer.ts
|
||||
!/editor/renderer/index.tsx
|
||||
/.astro
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -159,7 +159,8 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
{hideRenderer && (
|
||||
<EmptyRoadmap
|
||||
roadmapId={roadmapId}
|
||||
canManage={roadmap.canManage}
|
||||
// @ts-ignore
|
||||
canManage={roadmap?.canManage}
|
||||
className="grow"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'>·</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>
|
||||
|
||||
@@ -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'>·</span>
|
||||
</>
|
||||
@@ -39,7 +39,7 @@ return undefined;
|
||||
<span class='mx-1.5'>·</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>
|
||||
|
||||
@@ -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'>
|
||||
·
|
||||
{new Date(frontmatter.date).toLocaleString('default', {
|
||||
{new Date(frontmatter.date!).toLocaleString('default', {
|
||||
month: 'long',
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'>·</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) => (
|
||||
|
||||
@@ -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'>·</span>
|
||||
<span class='capitalize'>Illustrated Video</span>
|
||||
|
||||
@@ -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'>
|
||||
·
|
||||
@@ -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'> »</span>
|
||||
<span class='block text-xs text-gray-400 sm:hidden'> »</span>
|
||||
</a>
|
||||
|
||||
23
src/content/author.ts
Normal file
23
src/content/author.ts
Normal 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
16
src/content/changelog.ts
Normal 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
15
src/content/config.ts
Normal 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
25
src/content/guide.ts
Normal 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
28
src/content/project.ts
Normal 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
Reference in New Issue
Block a user