Skip to content

Commit

Permalink
feat(nx-dev): add video course visualizer
Browse files Browse the repository at this point in the history
  • Loading branch information
juristr committed Oct 31, 2024
1 parent d960ddd commit 2f2dcb5
Show file tree
Hide file tree
Showing 37 changed files with 1,101 additions and 9 deletions.
15 changes: 15 additions & 0 deletions docs/courses/pnpm-nx-next/course.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: 'From PNPM Workspaces to Distributed CI'
description: ''
slug: nx-19-8-update
authors: [Juri Strumpflohner]
cover_image: /blog/images/2024-09-20/thumbnail.png
---

**Key takeaways:**

- add Nx to an existing PNPM workspace
- fine tune your repo with local caching, defining task piplines and establishing dependencies among projects
- connect and configure your workspace with Nx Cloud
- learn how to debug cache misses and control remote cache access with Nx Cloud
- automatically split your Playwright e2e tests to optimize CI time from 20 minutes to 9 minutes
8 changes: 8 additions & 0 deletions docs/courses/pnpm-nx-next/lessons/00-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: 'Overview'
videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
---

# Overview

Here's some text related to the overview
28 changes: 28 additions & 0 deletions docs/courses/pnpm-nx-next/lessons/01-nx-init.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: 'Initialize the workspace'
videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
duration: '04:20'
---

In this lesson we're going to look into how to initialize a new Nx workspace.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
8 changes: 8 additions & 0 deletions docs/courses/pnpm-nx-next/lessons/02-configure-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: 'Configure caching'
videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
---

# Configure caching

...
16 changes: 16 additions & 0 deletions nx-dev/data-access-courses/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "data-access-courses",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "nx-dev/data-access-courses/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["nx-dev/data-access-courses/**/*.ts"]
}
}
},
"tags": []
}
1 change: 1 addition & 0 deletions nx-dev/data-access-courses/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/courses.api';
128 changes: 128 additions & 0 deletions nx-dev/data-access-courses/src/lib/courses.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { readFile, readdir } from 'fs/promises';
import { join } from 'path';
import { extractFrontmatter } from '@nx/nx-dev/ui-markdoc';
import { readFileSync } from 'fs';
import type { BlogAuthor } from '@nx/nx-dev/data-access-documents/node-only';

export interface Course {
id: string;
title: string;
description: string;
content: string;
authors: BlogAuthor[];
repository?: string;
lessons: Lesson[];
filePath: string;
totalDuration: string;
}

export interface Lesson {
id: string;
title: string;
description: string;
videoUrl: string;
duration: string;
filePath: string;
}

function calculateTotalDuration(lessons: Lesson[]): string {
const totalMinutes = lessons.reduce((total, lesson) => {
if (!lesson.duration) return total;
const [minutes, seconds] = lesson.duration.split(':').map(Number);
return total + minutes + seconds / 60;
}, 0);

const hours = Math.floor(totalMinutes / 60);
const minutes = Math.round(totalMinutes % 60);

if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}

export class CoursesApi {
constructor(
private readonly options: {
coursesRoot: string;
}
) {
if (!options.coursesRoot) {
throw new Error('courses root cannot be undefined');
}
}

async getAllCourses(): Promise<Course[]> {
const courseFolders = await readdir(this.options.coursesRoot);
const courses = await Promise.all(
courseFolders.map((folder) => this.getCourse(folder))
);
return courses;
}

// TODO: move to shared lib
private readonly blogRoot = 'public/documentation/blog';

async getCourse(folderName: string): Promise<Course> {
const authors = JSON.parse(
readFileSync(join(this.blogRoot, 'authors.json'), 'utf8')
);
const coursePath = join(this.options.coursesRoot, folderName);
const courseFilePath = join(coursePath, 'course.md');

const content = await readFile(courseFilePath, 'utf-8');
const frontmatter = extractFrontmatter(content);

const lessonFolders = await readdir(coursePath);
const lessons = await Promise.all(
lessonFolders
.filter((folder) => folder !== 'course.md')
.map((folder) => this.getLessons(folderName, folder))
);
const flattenedLessons = lessons.flat();

return {
id: folderName,
title: frontmatter.title,
description: frontmatter.description,
content,
authors: authors.filter((author: { name: string }) =>
frontmatter.authors.includes(author.name)
),
repository: frontmatter.repository,
lessons: flattenedLessons,
filePath: courseFilePath,
totalDuration: calculateTotalDuration(flattenedLessons),
};
}

private async getLessons(
courseId: string,
lessonFolder: string
): Promise<Lesson[]> {
const lessonPath = join(this.options.coursesRoot, courseId, lessonFolder);
const lessonFiles = await readdir(lessonPath);

const lessons = await Promise.all(
lessonFiles.map(async (file) => {
if (!file.endsWith('.md')) return null;
const filePath = join(lessonPath, file);
const content = await readFile(filePath, 'utf-8');
const frontmatter = extractFrontmatter(content);
if (!frontmatter || !frontmatter.title) {
throw new Error(`Lesson ${lessonFolder}/${file} has no title`);
}
return {
id: `${lessonFolder}-${file.replace('.md', '')}`,
title: frontmatter.title,
description: content,
videoUrl: frontmatter.videoUrl || null,
duration: frontmatter.duration || null,
filePath,
};
})
);

return lessons.filter((lesson): lesson is Lesson => lesson !== null);
}
}
17 changes: 17 additions & 0 deletions nx-dev/data-access-courses/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"extends": "../../tsconfig.base.json"
}
9 changes: 9 additions & 0 deletions nx-dev/data-access-courses/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
51 changes: 51 additions & 0 deletions nx-dev/nx-dev/app/courses/[courseId]/[lessonId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { coursesApi } from 'nx-dev/nx-dev/lib/courses.api';
import { DefaultLayout, YouTube } from '@nx/nx-dev/ui-common';
import { LessonPlayer } from '@nx/nx-dev/ui-courses';
import { Metadata } from 'next';

interface LessonPageProps {
params: { courseId: string; lessonId: string };
}

export async function generateMetadata({
params,
}: LessonPageProps): Promise<Metadata> {
const course = await coursesApi.getCourse(params.courseId);
const lesson = course.lessons.find((l) => l.id === params.lessonId);

if (!lesson) {
return {
title: 'Lesson Not Found',
};
}

return {
title: `${lesson.title} | ${course.title} | Nx Courses`,
description: lesson.description.substring(0, 160),
};
}

export async function generateStaticParams() {
const courses = await coursesApi.getAllCourses();
return courses.flatMap((course) =>
course.lessons.map((lesson) => ({
courseId: course.id,
lessonId: lesson.id,
}))
);
}

export default async function LessonPage({ params }: LessonPageProps) {
const course = await coursesApi.getCourse(params.courseId);
const lesson = course.lessons.find((l) => l.id === params.lessonId);

if (!lesson) {
return <div>Lesson not found</div>;
}

return (
<DefaultLayout hideHeader hideFooter>
<LessonPlayer course={course} lesson={lesson} />
</DefaultLayout>
);
}
58 changes: 58 additions & 0 deletions nx-dev/nx-dev/app/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Metadata, ResolvingMetadata } from 'next';
import { coursesApi } from 'nx-dev/nx-dev/lib/courses.api';
import { CourseDetails } from '@nx/nx-dev/ui-courses';
import { DefaultLayout } from '@nx/nx-dev/ui-common';

interface CourseDetailProps {
params: { courseId: string };
}

export async function generateMetadata(
{ params: { courseId } }: CourseDetailProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const course = await coursesApi.getCourse(courseId);
const previousImages = (await parent).openGraph?.images ?? [];

return {
title: `${course.title} | Nx Courses`,
description: course.description,
openGraph: {
url: `https://nx.dev/courses/${courseId}`,
title: course.title,
description: course.description,
images: [
{
url: '/path/to/default/course/image.png', // Add a default course image
width: 800,
height: 421,
alt: 'Nx Course: ' + course.title,
type: 'image/png',
},
...previousImages,
],
},
};
}

export async function generateStaticParams() {
const courses = await coursesApi.getAllCourses();
return courses.map((course) => {
return { courseId: course.id };
});
}

export default async function CourseDetail({
params: { courseId },
}: CourseDetailProps) {
const course = await coursesApi.getCourse(courseId);
return course ? (
<>
{/* This empty div is necessary as app router does not automatically scroll on route changes */}
<div></div>
<DefaultLayout>
<CourseDetails course={course} />
</DefaultLayout>
</>
) : null;
}
42 changes: 42 additions & 0 deletions nx-dev/nx-dev/app/courses/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { DefaultLayout } from '@nx/nx-dev/ui-common';
import { CourseOverview, CourseHero } from '@nx/nx-dev/ui-video-courses';
import { coursesApi } from '../../lib/courses.api';

import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Nx Video Courses',
description:
'Explore our comprehensive video courses to master Nx and boost your development skills.',
openGraph: {
url: 'https://nx.dev/courses',
title: 'Nx Video Courses',
description:
'Explore our comprehensive video courses to master Nx and boost your development skills.',
images: [
{
url: 'https://nx.dev/socials/nx-courses-media.png',
width: 800,
height: 421,
alt: 'Nx Video Courses: Master Nx through comprehensive tutorials',
type: 'image/jpeg',
},
],
siteName: 'NxDev',
type: 'website',
},
};

export default async function CoursesPage(): Promise<JSX.Element> {
const courses = await coursesApi.getAllCourses();

return (
<DefaultLayout>
<CourseHero />
<div className="mt-8">
<CourseOverview courses={courses} />
</div>
</DefaultLayout>
);
}
5 changes: 5 additions & 0 deletions nx-dev/nx-dev/lib/courses.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CoursesApi } from '@nx/nx-dev/data-access-courses';

export const coursesApi = new CoursesApi({
coursesRoot: 'public/documentation/courses',
});
Loading

0 comments on commit 2f2dcb5

Please sign in to comment.