feat(nx-dev): webinar page (#29913)
Adds a webinar page and a script to pull the webinar content from Notion
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: 'Monorepos: the Benefits, Challenges, and Importance of Tooling Support '
|
||||||
|
description: 'Learn how monorepos and better tooling can help you overcome challenges in software development like scalability, maintenance, communication, and cost.'
|
||||||
|
slug: 'monorepos-the-benefits-challenges-and-importance-of-tooling-support'
|
||||||
|
authors: ['Juri Strumpflohner']
|
||||||
|
tags: [webinar]
|
||||||
|
cover_image: /blog/images/2024-01-24/january-webinar-card.png
|
||||||
|
status: Past - Gated
|
||||||
|
registrationUrl: https://go.nx.dev/january-webinar
|
||||||
|
---
|
||||||
|
|
||||||
|
Presented by Juri Strumpflohner
|
||||||
|
|
||||||
|
Learn how monorepos and better tooling can help you overcome challenges in software development like scalability, maintenance, communication, and cost.
|
||||||
|
|
||||||
|
{% call-to-action title="Download the recording" url="https://go.nx.dev/january-webinar" description="Sign up to gain access" /%}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: "Nx Agents Walkthrough:
|
||||||
|
Effortlessly Fast CI Built for Monorepos"
|
||||||
|
description: "Learn how you can streamline your existing CI config to its absolute simplest form, reducing CI times from 30 minutes to ~5 minutes with Nx Agents. "
|
||||||
|
slug: 'nx-agents-walkthrougheffortlessly-fast-ci-built-for-monorepos'
|
||||||
|
authors: ['Rares Matei']
|
||||||
|
tags: [webinar]
|
||||||
|
cover_image: /blog/images/2024-03-11/march-webinar.png
|
||||||
|
status: Past - Gated
|
||||||
|
registrationUrl: https://go.nx.dev/march-webinar
|
||||||
|
---
|
||||||
|
|
||||||
|
Presented by Rares Matei
|
||||||
|
|
||||||
|
Learn how you can streamline your existing CI config to its absolute simplest form, reducing CI times from 30 minutes to ~5 minutes with Nx Agents.
|
||||||
|
|
||||||
|
{% call-to-action title="Download the recording" url="https://go.nx.dev/march-webinar" description="Sign up to gain access" /%}
|
||||||
16
docs/blog/2024-04-17-making-the-argument-for-monorepos.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: 'Making the Argument for Monorepos'
|
||||||
|
description: 'Trying to convince your colleagues to use a monorepo? Already using a monorepo and need to justify that decision? Check out this webinar to learn 7 essential reasons for using monorepos and bust a few myths and misconceptions along the way.'
|
||||||
|
slug: 'making-the-argument-for-monorepos'
|
||||||
|
authors: ['Miroslav Jonaš']
|
||||||
|
tags: [webinar]
|
||||||
|
cover_image: /blog/images/2024-04-17/april-webinar.png
|
||||||
|
status: Past - Gated
|
||||||
|
registrationUrl: https://go.nx.dev/april-webinar
|
||||||
|
---
|
||||||
|
|
||||||
|
Presented by Miroslav Jonaš
|
||||||
|
|
||||||
|
Trying to convince your colleagues to use a monorepo? Already using a monorepo and need to justify that decision? Check out this webinar to learn 7 essential reasons for using monorepos and bust a few myths and misconceptions along the way.
|
||||||
|
|
||||||
|
{% call-to-action title="Download the recording" url="https://go.nx.dev/april-webinar" description="Sign up to gain access" /%}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: "Monorepos and CI can be a Mess - Here's How Nx and Nx Cloud Fixed It"
|
||||||
|
description: "Continuous Integration (CI) in monorepos can be notoriously slow and unreliable, quickly become a bottleneck for scaling monorepos.
|
||||||
|
Learn how Nx and Nx Cloud's new task-based approach ensures fast, resilient, and efficient CI for your monorepo projects."
|
||||||
|
slug: 'monorepos-and-ci-can-be-a-mess-heres-how-nx-and-nx-cloud-fixed-it'
|
||||||
|
authors: ['Juri Strumpflohner']
|
||||||
|
tags: [webinar]
|
||||||
|
cover_image: /blog/images/2024-06-26/June-Webinar-card.png
|
||||||
|
status: Past - Gated
|
||||||
|
registrationUrl: https://go.nx.dev/june-webinar
|
||||||
|
---
|
||||||
|
|
||||||
|
Presented by Juri Strumpflohner
|
||||||
|
|
||||||
|
Continuous Integration (CI) in monorepos can be notoriously slow and unreliable, quickly become a bottleneck for scaling monorepos.
|
||||||
|
Learn how Nx and Nx Cloud's new task-based approach ensures fast, resilient, and efficient CI for your monorepo projects.
|
||||||
|
|
||||||
|
{% call-to-action title="Download the recording" url="https://go.nx.dev/june-webinar" description="Sign up to gain access" /%}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: 'Nx Cloud: Scale Your CI and Team with Ease'
|
||||||
|
description: 'Learn how you can attain fast, reliable CI and better coordination across your technical organization with Nx Cloud, and see our new multi-workspace features for organizational scaling in action.'
|
||||||
|
slug: 'nx-cloud-scale-your-ci-and-team-with-ease'
|
||||||
|
authors: ['Nicole Oliver', 'Rares Matei', 'James Henry']
|
||||||
|
tags: [webinar]
|
||||||
|
cover_image: /blog/images/2025-01-22/Jan-webinar-image.png
|
||||||
|
time: 1-2pm ET/6-7pm UTC
|
||||||
|
status: Upcoming
|
||||||
|
registrationUrl: https://go.nx.dev/jan2025-webinar
|
||||||
|
---
|
||||||
|
|
||||||
|
**Jan 22, 2025 - 1-2pm ET/6-7pm UTC**
|
||||||
|
|
||||||
|
Presented by Nicole Oliver, Rares Matei, and James Henry
|
||||||
|
|
||||||
|
Learn how you can attain fast, reliable CI and better coordination across your technical organization with Nx Cloud, and see our new multi-workspace features for organizational scaling in action.
|
||||||
|
|
||||||
|
{% call-to-action title="Register today!" url="https://go.nx.dev/jan2025-webinar" description="Save your spot" /%}
|
||||||
@ -141,5 +141,23 @@
|
|||||||
"image": "/blog/images/Nicolas Beaussart.jpeg",
|
"image": "/blog/images/Nicolas Beaussart.jpeg",
|
||||||
"twitter": "beaussan",
|
"twitter": "beaussan",
|
||||||
"github": "beaussan"
|
"github": "beaussan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nicole Oliver",
|
||||||
|
"image": "/blog/images/Nicole Oliver.jpeg",
|
||||||
|
"twitter": "nixcodes",
|
||||||
|
"github": "nixallover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rares Matei",
|
||||||
|
"image": "/blog/images/Rares Matei.jpeg",
|
||||||
|
"twitter": "__rares",
|
||||||
|
"github": "rarmatei"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "James Henry",
|
||||||
|
"image": "/blog/images/James Henry.jpeg",
|
||||||
|
"twitter": "MrJamesHenry",
|
||||||
|
"github": "JamesHenry"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
docs/blog/images/2024-01-24/january-webinar-card.png
Normal file
|
After Width: | Height: | Size: 632 KiB |
BIN
docs/blog/images/2024-03-11/march-webinar.png
Normal file
|
After Width: | Height: | Size: 599 KiB |
BIN
docs/blog/images/2024-04-17/april-webinar.png
Normal file
|
After Width: | Height: | Size: 600 KiB |
BIN
docs/blog/images/2024-06-26/June-Webinar-card.png
Normal file
|
After Width: | Height: | Size: 534 KiB |
BIN
docs/blog/images/2025-01-22/Jan-webinar-image.png
Normal file
|
After Width: | Height: | Size: 621 KiB |
BIN
docs/blog/images/authors/James Henry.jpeg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/blog/images/authors/Nicole Oliver.jpeg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/blog/images/authors/Rares Matei.jpeg
Normal file
|
After Width: | Height: | Size: 21 KiB |
@ -55,9 +55,11 @@ export class BlogApi {
|
|||||||
title: frontmatter.title ?? null,
|
title: frontmatter.title ?? null,
|
||||||
description: frontmatter.description ?? null,
|
description: frontmatter.description ?? null,
|
||||||
authors: authors.filter((author) =>
|
authors: authors.filter((author) =>
|
||||||
frontmatter.authors.includes(author.name)
|
frontmatter.authors?.includes(author.name)
|
||||||
),
|
),
|
||||||
date: this.calculateDate(file, frontmatter),
|
date: this.calculateDate(file, frontmatter),
|
||||||
|
time: frontmatter.time,
|
||||||
|
status: frontmatter.status,
|
||||||
cover_image: frontmatter.cover_image
|
cover_image: frontmatter.cover_image
|
||||||
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
|
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
|
||||||
: null,
|
: null,
|
||||||
@ -69,6 +71,7 @@ export class BlogApi {
|
|||||||
filePath,
|
filePath,
|
||||||
slug,
|
slug,
|
||||||
youtubeUrl: frontmatter.youtubeUrl,
|
youtubeUrl: frontmatter.youtubeUrl,
|
||||||
|
registrationUrl: frontmatter.registrationUrl,
|
||||||
podcastYoutubeId: frontmatter.podcastYoutubeId,
|
podcastYoutubeId: frontmatter.podcastYoutubeId,
|
||||||
podcastSpotifyId: frontmatter.podcastSpotifyId,
|
podcastSpotifyId: frontmatter.podcastSpotifyId,
|
||||||
podcastIHeartUrl: frontmatter.podcastIHeartUrl,
|
podcastIHeartUrl: frontmatter.podcastIHeartUrl,
|
||||||
|
|||||||
16
nx-dev/data-access-documents/src/lib/webinar.api.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { BlogApi } from './blog.api';
|
||||||
|
import { WebinarDataEntry } from './webinar.model';
|
||||||
|
|
||||||
|
export class WebinarApi {
|
||||||
|
_blogApi: BlogApi;
|
||||||
|
|
||||||
|
constructor(options: { blogApi: BlogApi }) {
|
||||||
|
this._blogApi = options.blogApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebinarBlogs(): Promise<WebinarDataEntry[]> {
|
||||||
|
return await this._blogApi.getBlogs((post) =>
|
||||||
|
post.tags.map((t) => t.toLowerCase()).includes('webinar')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
nx-dev/data-access-documents/src/lib/webinar.model.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { BlogPostDataEntry } from './blog.model';
|
||||||
|
|
||||||
|
export interface WebinarDataEntry extends BlogPostDataEntry {
|
||||||
|
status?: 'Upcoming' | 'Past - Gated' | 'Past - Ungated';
|
||||||
|
time?: string;
|
||||||
|
registrationUrl?: string;
|
||||||
|
}
|
||||||
@ -6,3 +6,5 @@ export * from './lib/blog.model';
|
|||||||
export * from './lib/tags.api';
|
export * from './lib/tags.api';
|
||||||
export * from './lib/podcast.model';
|
export * from './lib/podcast.model';
|
||||||
export * from './lib/podcast.api';
|
export * from './lib/podcast.api';
|
||||||
|
export * from './lib/webinar.model';
|
||||||
|
export * from './lib/webinar.api';
|
||||||
|
|||||||
39
nx-dev/nx-dev/app/webinar/page.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import { webinarApi } from '../../lib/webinar.api';
|
||||||
|
import { DefaultLayout } from '@nx/nx-dev/ui-common';
|
||||||
|
import { Hero, WebinarList } from '@nx/nx-dev/ui-webinar';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Nx Webinar - Updates from the Nx & Nx Cloud team',
|
||||||
|
description: 'Latest webinars from the Nx & Nx Cloud core team',
|
||||||
|
openGraph: {
|
||||||
|
url: 'https://nx.dev/webinar',
|
||||||
|
title: 'Nx Webinar - Updates from the Nx & Nx Cloud team',
|
||||||
|
description:
|
||||||
|
'Stay updated with the latest webinars from the Nx & Nx Cloud team.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://nx.dev/socials/nx-media.png',
|
||||||
|
width: 800,
|
||||||
|
height: 421,
|
||||||
|
alt: 'Nx: Smart Monorepos · Fast CI',
|
||||||
|
type: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
siteName: 'Nx',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getWebinars() {
|
||||||
|
return await webinarApi.getWebinarBlogs();
|
||||||
|
}
|
||||||
|
export default async function Page() {
|
||||||
|
const webinars = await getWebinars();
|
||||||
|
return (
|
||||||
|
<DefaultLayout>
|
||||||
|
<Hero />
|
||||||
|
<WebinarList webinars={webinars} />
|
||||||
|
</DefaultLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
nx-dev/nx-dev/lib/webinar.api.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { blogApi } from './blog.api';
|
||||||
|
import { WebinarApi } from '@nx/nx-dev/data-access-documents/node-only';
|
||||||
|
|
||||||
|
export const webinarApi = new WebinarApi({ blogApi });
|
||||||
@ -5,9 +5,10 @@ import Image from 'next/image';
|
|||||||
|
|
||||||
export interface BlogEntryProps {
|
export interface BlogEntryProps {
|
||||||
post: BlogPostDataEntry;
|
post: BlogPostDataEntry;
|
||||||
|
overrideLink?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlogEntry({ post }: BlogEntryProps) {
|
export function BlogEntry({ post, overrideLink }: BlogEntryProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full transform-gpu flex-col overflow-hidden rounded-2xl border border-slate-200 shadow transition-all duration-300 ease-in-out hover:scale-[1.02] hover:shadow-lg dark:border-slate-800">
|
<div className="relative flex h-full transform-gpu flex-col overflow-hidden rounded-2xl border border-slate-200 shadow transition-all duration-300 ease-in-out hover:scale-[1.02] hover:shadow-lg dark:border-slate-800">
|
||||||
{post.cover_image && (
|
{post.cover_image && (
|
||||||
@ -25,7 +26,7 @@ export function BlogEntry({ post }: BlogEntryProps) {
|
|||||||
<div className="flex flex-col gap-1 p-4">
|
<div className="flex flex-col gap-1 p-4">
|
||||||
<BlogAuthors authors={post.authors} />
|
<BlogAuthors authors={post.authors} />
|
||||||
<Link
|
<Link
|
||||||
href={`/blog/${post.slug}`}
|
href={overrideLink ? overrideLink : `/blog/${post.slug}`}
|
||||||
title={post.title}
|
title={post.title}
|
||||||
className="text-balance text-lg font-semibold text-slate-900 dark:text-white"
|
className="text-balance text-lg font-semibold text-slate-900 dark:text-white"
|
||||||
prefetch={false}
|
prefetch={false}
|
||||||
|
|||||||
@ -203,7 +203,7 @@ export const learnItems: MenuItem[] = [
|
|||||||
{
|
{
|
||||||
name: 'Webinars',
|
name: 'Webinars',
|
||||||
description: null,
|
description: null,
|
||||||
href: 'https://go.nx.dev/jan2025-webinar',
|
href: '/webinar',
|
||||||
icon: ComputerDesktopIcon,
|
icon: ComputerDesktopIcon,
|
||||||
isNew: false,
|
isNew: false,
|
||||||
isHighlight: false,
|
isHighlight: false,
|
||||||
|
|||||||
@ -58,6 +58,7 @@ import { Quote } from './lib/tags/quote.component';
|
|||||||
import { quote } from './lib/tags/quote.schema';
|
import { quote } from './lib/tags/quote.schema';
|
||||||
import { metrics } from './lib/tags/metrics.schema';
|
import { metrics } from './lib/tags/metrics.schema';
|
||||||
import { Metrics } from './lib/tags/metrics.component';
|
import { Metrics } from './lib/tags/metrics.component';
|
||||||
|
export { CallToAction };
|
||||||
|
|
||||||
export const getMarkdocCustomConfig = (
|
export const getMarkdocCustomConfig = (
|
||||||
documentFilePath: string,
|
documentFilePath: string,
|
||||||
|
|||||||
12
nx-dev/ui-webinar/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nx/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
||||||
18
nx-dev/ui-webinar/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
nx-dev/ui-webinar/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# ui-webinar
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test ui-webinar` to execute the unit tests via [Jest](https://jestjs.io).
|
||||||
9
nx-dev/ui-webinar/project.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "ui-webinar",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "nx-dev/ui-webinar/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"// targets": "to see all targets run: nx show project ui-webinar --web",
|
||||||
|
"targets": {}
|
||||||
|
}
|
||||||
2
nx-dev/ui-webinar/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './lib/hero';
|
||||||
|
export * from './lib/webinar-list';
|
||||||
21
nx-dev/ui-webinar/src/lib/hero.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { SectionHeading } from '@nx/nx-dev/ui-common';
|
||||||
|
|
||||||
|
export function Hero(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-7xl">
|
||||||
|
<div className="grid grid-cols-4 gap-x-4 px-8 lg:grid-cols-12 lg:gap-x-6">
|
||||||
|
<div className="col-span-full md:col-span-4 lg:col-span-6">
|
||||||
|
<SectionHeading as="h1" variant="title">
|
||||||
|
Nx Webinars
|
||||||
|
</SectionHeading>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<SectionHeading as="p" variant="subtitle" className="mt-8">
|
||||||
|
In-depth explanations and interactive Q&A sessions with Nx team
|
||||||
|
members
|
||||||
|
</SectionHeading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
nx-dev/ui-webinar/src/lib/webinar-list-item.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { BlogAuthors } from '@nx/nx-dev/ui-blog';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import type { WebinarDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
|
||||||
|
|
||||||
|
export interface WebinarListItemProps {
|
||||||
|
webinar: WebinarDataEntry;
|
||||||
|
episode: number;
|
||||||
|
}
|
||||||
|
export function WebinarListItem({ webinar, episode }: WebinarListItemProps) {
|
||||||
|
const formattedDate = new Date(webinar.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
const authorsList = (
|
||||||
|
webinar.authors.length > 1
|
||||||
|
? webinar.authors.map((a, i) =>
|
||||||
|
i === webinar.authors.length - 1 ? 'and ' + a.name : a.name
|
||||||
|
)
|
||||||
|
: webinar.authors.map((a) => a.name)
|
||||||
|
).join(', ');
|
||||||
|
const link =
|
||||||
|
(webinar.status === 'Past - Ungated'
|
||||||
|
? webinar.youtubeUrl
|
||||||
|
: webinar.registrationUrl) || '';
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={webinar.slug}
|
||||||
|
className="border-b border-slate-200 py-5 text-sm last:border-0 dark:border-slate-800 dark:before:bg-slate-800/50"
|
||||||
|
>
|
||||||
|
<Link href={link} prefetch={false}>
|
||||||
|
<h3 className="text-balance text-lg text-slate-500 sm:w-8/12 dark:text-white">
|
||||||
|
{webinar.title}
|
||||||
|
</h3>
|
||||||
|
</Link>
|
||||||
|
<span className="my-4 block">
|
||||||
|
<time dateTime={webinar.date}>{formattedDate}</time>
|
||||||
|
</span>
|
||||||
|
<span className="my-4 block">
|
||||||
|
<span className="inline-block">
|
||||||
|
<BlogAuthors authors={webinar.authors} showAuthorDetails={false} />
|
||||||
|
</span>
|
||||||
|
<span className="mx-2 inline-block">{authorsList}</span>
|
||||||
|
</span>
|
||||||
|
<p className="my-2">{webinar.description}</p>
|
||||||
|
<Link href={link} prefetch={false}>
|
||||||
|
<span className="my-4 text-balance text-slate-500 sm:w-8/12 dark:text-white">
|
||||||
|
{webinar.status === 'Past - Gated'
|
||||||
|
? 'Sign up to view the recording'
|
||||||
|
: 'Watch the recording'}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
69
nx-dev/ui-webinar/src/lib/webinar-list.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { WebinarDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
|
||||||
|
import { BlogEntry } from '@nx/nx-dev/ui-blog';
|
||||||
|
import { WebinarListItem } from './webinar-list-item';
|
||||||
|
import { CallToAction } from '@nx/nx-dev/ui-markdoc';
|
||||||
|
|
||||||
|
export interface WebinarListProps {
|
||||||
|
webinars: WebinarDataEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WebinarList({ webinars }: WebinarListProps): JSX.Element {
|
||||||
|
return webinars.length < 1 ? (
|
||||||
|
<div>
|
||||||
|
<h2 className="mt-32 text-center text-xl font-semibold text-slate-500 sm:text-2xl xl:mb-24 dark:text-white ">
|
||||||
|
No webinars as yet but stay tuned!
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mx-auto max-w-7xl px-8">
|
||||||
|
{webinars
|
||||||
|
.filter((w) => w.status === 'Upcoming')
|
||||||
|
.map((webinar, index) => {
|
||||||
|
const authorsList = (
|
||||||
|
webinar.authors.length > 1
|
||||||
|
? webinar.authors.map((a, i) =>
|
||||||
|
i === webinar.authors.length - 1 ? 'and ' + a.name : a.name
|
||||||
|
)
|
||||||
|
: webinar.authors.map((a) => a.name)
|
||||||
|
).join(', ');
|
||||||
|
const dateAndTime =
|
||||||
|
new Date(webinar.date).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
}) + (webinar.time ? ' - ' + webinar.time : '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 w-full max-w-xl">
|
||||||
|
<BlogEntry
|
||||||
|
post={webinar}
|
||||||
|
overrideLink={webinar.registrationUrl}
|
||||||
|
></BlogEntry>
|
||||||
|
<p className="my-4 font-bold">{dateAndTime}</p>
|
||||||
|
<p className="my-4">Presented by {authorsList}</p>
|
||||||
|
<p className="my-4">{webinar.description}</p>
|
||||||
|
{webinar.registrationUrl && (
|
||||||
|
<div className="max-w-md">
|
||||||
|
<CallToAction
|
||||||
|
title="Register today!"
|
||||||
|
description="Save your spot"
|
||||||
|
url={webinar.registrationUrl}
|
||||||
|
></CallToAction>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="mt-20 border-b-2 border-slate-300 pb-3 text-lg dark:border-slate-700">
|
||||||
|
<h2 className="font-semibold">Past Webinars</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{webinars
|
||||||
|
.filter((w) => w.status !== 'Upcoming')
|
||||||
|
.map((w, index) => (
|
||||||
|
<WebinarListItem key={w.slug} webinar={w} episode={index + 1} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
nx-dev/ui-webinar/tsconfig.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
24
nx-dev/ui-webinar/tsconfig.lib.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
|
||||||
|
"@nx/react/typings/cssmodule.d.ts",
|
||||||
|
"@nx/react/typings/image.d.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"jest.config.ts",
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.test.ts",
|
||||||
|
"src/**/*.spec.tsx",
|
||||||
|
"src/**/*.test.tsx",
|
||||||
|
"src/**/*.spec.js",
|
||||||
|
"src/**/*.test.js",
|
||||||
|
"src/**/*.spec.jsx",
|
||||||
|
"src/**/*.test.jsx"
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||||
|
}
|
||||||
@ -67,6 +67,7 @@
|
|||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"@ngrx/router-store": "19.0.0",
|
"@ngrx/router-store": "19.0.0",
|
||||||
"@ngrx/store": "19.0.0",
|
"@ngrx/store": "19.0.0",
|
||||||
|
"@notionhq/client": "^2.2.15",
|
||||||
"@nuxt/kit": "^3.10.0",
|
"@nuxt/kit": "^3.10.0",
|
||||||
"@nuxt/schema": "^3.10.0",
|
"@nuxt/schema": "^3.10.0",
|
||||||
"@nx/angular": "20.4.0-beta.2",
|
"@nx/angular": "20.4.0-beta.2",
|
||||||
|
|||||||
420
pnpm-lock.yaml
generated
271
scripts/documentation/load-webinars.ts
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
import { Client } from '@notionhq/client';
|
||||||
|
import {
|
||||||
|
createWriteStream,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from 'node:fs';
|
||||||
|
import { get } from 'node:https';
|
||||||
|
import { dirname } from 'node:path';
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import { slugify } from '../../nx-dev/feature-package-schema-viewer/src/lib/slugify.utils';
|
||||||
|
const notion = new Client({ auth: process.env.NOTION_KEY });
|
||||||
|
|
||||||
|
const BLOG_ROOT = './docs/blog';
|
||||||
|
|
||||||
|
interface RichTextProperty {
|
||||||
|
type: 'rich_text';
|
||||||
|
rich_text: RichTextItem[];
|
||||||
|
}
|
||||||
|
interface RichTextItem {
|
||||||
|
type: 'text';
|
||||||
|
text: {
|
||||||
|
content: string;
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
annotations: {
|
||||||
|
bold: boolean;
|
||||||
|
italic: boolean;
|
||||||
|
strikethrough: boolean;
|
||||||
|
underline: boolean;
|
||||||
|
code: boolean;
|
||||||
|
color: 'default' | string;
|
||||||
|
};
|
||||||
|
plain_text: string;
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
interface DateProperty {
|
||||||
|
type: 'date';
|
||||||
|
date: { start: string; end?: string; time_zone?: string };
|
||||||
|
}
|
||||||
|
interface UrlProperty {
|
||||||
|
type: 'url';
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
interface FilesProperty {
|
||||||
|
type: 'files';
|
||||||
|
files: {
|
||||||
|
name: string;
|
||||||
|
type: 'file';
|
||||||
|
file: {
|
||||||
|
url: string;
|
||||||
|
expiry_time: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
interface SelectProperty {
|
||||||
|
type: 'select';
|
||||||
|
select: { id: string; name: string; color: string };
|
||||||
|
}
|
||||||
|
interface TitleProperty {
|
||||||
|
type: 'title';
|
||||||
|
title: RichTextItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebinarResponse {
|
||||||
|
Description: RichTextProperty;
|
||||||
|
Date: DateProperty;
|
||||||
|
['YouTube Link']: UrlProperty;
|
||||||
|
['Webinar Card Image']: FilesProperty;
|
||||||
|
Status: SelectProperty;
|
||||||
|
['Speaker(s)']: RichTextProperty;
|
||||||
|
Time: RichTextProperty;
|
||||||
|
['Link to Landing Page']: UrlProperty;
|
||||||
|
Title: TitleProperty;
|
||||||
|
}
|
||||||
|
interface ProcessedWebinar {
|
||||||
|
Description: string;
|
||||||
|
Date: string;
|
||||||
|
['YouTube Link']: string;
|
||||||
|
['Webinar Card Image']: string[];
|
||||||
|
Status: string;
|
||||||
|
['Speaker(s)']: RichTextProperty;
|
||||||
|
Time: RichTextProperty;
|
||||||
|
['Link to Landing Page']: UrlProperty;
|
||||||
|
Title: TitleProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const propertyParsers = {
|
||||||
|
rich_text: (prop: RichTextProperty): string => {
|
||||||
|
return prop.rich_text
|
||||||
|
.map((item) => {
|
||||||
|
let text = item.text.content;
|
||||||
|
if (item.annotations.bold) {
|
||||||
|
text = `**${text}**`;
|
||||||
|
} else if (item.annotations.code) {
|
||||||
|
text = `\`${text}\``;
|
||||||
|
} else if (item.annotations.italic) {
|
||||||
|
text = `*${text}*`;
|
||||||
|
} else if (item.annotations.strikethrough) {
|
||||||
|
text = `~${text}~`;
|
||||||
|
} else if (item.annotations.underline) {
|
||||||
|
text = `_${text}_`;
|
||||||
|
}
|
||||||
|
if (item.href) {
|
||||||
|
text = `[${text}](${item.href})`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
},
|
||||||
|
select: (prop: SelectProperty): string => {
|
||||||
|
return prop.select.name;
|
||||||
|
},
|
||||||
|
title: (prop: TitleProperty): string => {
|
||||||
|
return propertyParsers.rich_text({
|
||||||
|
type: 'rich_text',
|
||||||
|
rich_text: prop.title,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
url: (prop: UrlProperty): string => {
|
||||||
|
return prop.url;
|
||||||
|
},
|
||||||
|
date: (prop: DateProperty): string => {
|
||||||
|
return prop.date.start;
|
||||||
|
},
|
||||||
|
files: (prop: FilesProperty): string => {
|
||||||
|
return prop.files.map((entry) => {
|
||||||
|
return entry.file.url;
|
||||||
|
})[0];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const response = await notion.databases.query({
|
||||||
|
database_id: '17c69f3c238780eb8ef6df32ce48f919',
|
||||||
|
});
|
||||||
|
response.results.forEach(async (entry: any) => {
|
||||||
|
const webinar: WebinarResponse = entry.properties;
|
||||||
|
const processedWebinar = Object.fromEntries(
|
||||||
|
Object.entries(webinar).map(([key, val]) => {
|
||||||
|
return [key, propertyParsers[val.type](val)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let cover_image = '';
|
||||||
|
const imageFiles = webinar['Webinar Card Image'].files;
|
||||||
|
function ensureDirectoryExistence(filePath) {
|
||||||
|
var directory = dirname(filePath);
|
||||||
|
if (existsSync(directory)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
ensureDirectoryExistence(directory);
|
||||||
|
mkdirSync(directory);
|
||||||
|
}
|
||||||
|
async function download(url: string, path: string) {
|
||||||
|
return new Promise(async (onSuccess) => {
|
||||||
|
get(url, async (res) => {
|
||||||
|
ensureDirectoryExistence(path);
|
||||||
|
const fileWriteStream = createWriteStream(path, {
|
||||||
|
autoClose: true,
|
||||||
|
flags: 'w',
|
||||||
|
});
|
||||||
|
await pipeline(res, fileWriteStream);
|
||||||
|
onSuccess('success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
download(
|
||||||
|
imageFiles[0].file.url,
|
||||||
|
BLOG_ROOT +
|
||||||
|
`/images/${webinar.Date.date.start}/${imageFiles[0].name.replaceAll(
|
||||||
|
' ',
|
||||||
|
'-'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'Downloaded image',
|
||||||
|
BLOG_ROOT +
|
||||||
|
`/images/${webinar.Date.date.start}/${imageFiles[0].name.replaceAll(
|
||||||
|
' ',
|
||||||
|
'-'
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
cover_image = `/blog/images/${
|
||||||
|
webinar.Date.date.start
|
||||||
|
}/${imageFiles[0].name.replaceAll(' ', '-')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webinarMarkdown = `---
|
||||||
|
title: "${processedWebinar.Title}"
|
||||||
|
description: "${processedWebinar.Description}"
|
||||||
|
slug: '${slugify(processedWebinar.Title)}'
|
||||||
|
authors: [${processedWebinar['Speaker(s)']
|
||||||
|
.replace(', and ', ', ')
|
||||||
|
.split(', ')
|
||||||
|
.map((author) => `'${author}'`)
|
||||||
|
.join(', ')}]
|
||||||
|
tags: [webinar]${
|
||||||
|
cover_image
|
||||||
|
? `
|
||||||
|
cover_image: ${cover_image}`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
processedWebinar['Time']
|
||||||
|
? `
|
||||||
|
time: ${processedWebinar['Time']}`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
processedWebinar['Status']
|
||||||
|
? `
|
||||||
|
status: ${processedWebinar['Status']}`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
processedWebinar['YouTube Link'] &&
|
||||||
|
processedWebinar['Status'] === 'Past - Ungated'
|
||||||
|
? `
|
||||||
|
youtubeUrl: ${processedWebinar['YouTube Link']}`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
processedWebinar['Link to Landing Page']
|
||||||
|
? `
|
||||||
|
registrationUrl: ${processedWebinar['Link to Landing Page']}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
${
|
||||||
|
processedWebinar.Time
|
||||||
|
? `**${new Date(
|
||||||
|
processedWebinar.Date + ' ' + new Date().toTimeString()
|
||||||
|
).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
})} - ${processedWebinar.Time}**`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
${
|
||||||
|
processedWebinar['Speaker(s)']
|
||||||
|
? `Presented by ${processedWebinar['Speaker(s)']}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
${processedWebinar.Description}
|
||||||
|
|
||||||
|
${
|
||||||
|
processedWebinar.Status === 'Upcoming'
|
||||||
|
? `{% call-to-action title="Register today!" url="${processedWebinar['Link to Landing Page']}" description="Save your spot" /%}`
|
||||||
|
: ''
|
||||||
|
}${
|
||||||
|
processedWebinar.Status === 'Past - Gated' &&
|
||||||
|
processedWebinar['Link to Landing Page']
|
||||||
|
? `{% call-to-action title="Download the recording" url="${processedWebinar['Link to Landing Page']}" description="Sign up to gain access" /%}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
BLOG_ROOT +
|
||||||
|
'/' +
|
||||||
|
processedWebinar.Date +
|
||||||
|
'-' +
|
||||||
|
slugify(processedWebinar.Title) +
|
||||||
|
'.md',
|
||||||
|
webinarMarkdown
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
main();
|
||||||
@ -119,6 +119,7 @@
|
|||||||
"@nx/nx-dev/ui-sponsor-card": ["nx-dev/ui-sponsor-card/src/index.ts"],
|
"@nx/nx-dev/ui-sponsor-card": ["nx-dev/ui-sponsor-card/src/index.ts"],
|
||||||
"@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"],
|
"@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"],
|
||||||
"@nx/nx-dev/ui-video-courses": ["nx-dev/ui-video-courses/src/index.ts"],
|
"@nx/nx-dev/ui-video-courses": ["nx-dev/ui-video-courses/src/index.ts"],
|
||||||
|
"@nx/nx-dev/ui-webinar": ["nx-dev/ui-webinar/src/index.ts"],
|
||||||
"@nx/nx-dev/util-ai": ["nx-dev/util-ai/src/index.ts"],
|
"@nx/nx-dev/util-ai": ["nx-dev/util-ai/src/index.ts"],
|
||||||
"@nx/playwright": ["packages/playwright/index.ts"],
|
"@nx/playwright": ["packages/playwright/index.ts"],
|
||||||
"@nx/playwright/*": ["packages/playwright/*"],
|
"@nx/playwright/*": ["packages/playwright/*"],
|
||||||
|
|||||||