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",
|
||||
"twitter": "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,
|
||||
description: frontmatter.description ?? null,
|
||||
authors: authors.filter((author) =>
|
||||
frontmatter.authors.includes(author.name)
|
||||
frontmatter.authors?.includes(author.name)
|
||||
),
|
||||
date: this.calculateDate(file, frontmatter),
|
||||
time: frontmatter.time,
|
||||
status: frontmatter.status,
|
||||
cover_image: frontmatter.cover_image
|
||||
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
|
||||
: null,
|
||||
@ -69,6 +71,7 @@ export class BlogApi {
|
||||
filePath,
|
||||
slug,
|
||||
youtubeUrl: frontmatter.youtubeUrl,
|
||||
registrationUrl: frontmatter.registrationUrl,
|
||||
podcastYoutubeId: frontmatter.podcastYoutubeId,
|
||||
podcastSpotifyId: frontmatter.podcastSpotifyId,
|
||||
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/podcast.model';
|
||||
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 {
|
||||
post: BlogPostDataEntry;
|
||||
overrideLink?: string;
|
||||
}
|
||||
|
||||
export function BlogEntry({ post }: BlogEntryProps) {
|
||||
export function BlogEntry({ post, overrideLink }: BlogEntryProps) {
|
||||
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">
|
||||
{post.cover_image && (
|
||||
@ -25,7 +26,7 @@ export function BlogEntry({ post }: BlogEntryProps) {
|
||||
<div className="flex flex-col gap-1 p-4">
|
||||
<BlogAuthors authors={post.authors} />
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
href={overrideLink ? overrideLink : `/blog/${post.slug}`}
|
||||
title={post.title}
|
||||
className="text-balance text-lg font-semibold text-slate-900 dark:text-white"
|
||||
prefetch={false}
|
||||
|
||||
@ -203,7 +203,7 @@ export const learnItems: MenuItem[] = [
|
||||
{
|
||||
name: 'Webinars',
|
||||
description: null,
|
||||
href: 'https://go.nx.dev/jan2025-webinar',
|
||||
href: '/webinar',
|
||||
icon: ComputerDesktopIcon,
|
||||
isNew: false,
|
||||
isHighlight: false,
|
||||
|
||||
@ -58,6 +58,7 @@ import { Quote } from './lib/tags/quote.component';
|
||||
import { quote } from './lib/tags/quote.schema';
|
||||
import { metrics } from './lib/tags/metrics.schema';
|
||||
import { Metrics } from './lib/tags/metrics.component';
|
||||
export { CallToAction };
|
||||
|
||||
export const getMarkdocCustomConfig = (
|
||||
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",
|
||||
"@ngrx/router-store": "19.0.0",
|
||||
"@ngrx/store": "19.0.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@nuxt/kit": "^3.10.0",
|
||||
"@nuxt/schema": "^3.10.0",
|
||||
"@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-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-webinar": ["nx-dev/ui-webinar/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/*"],
|
||||
|
||||