diff --git a/nx-dev/data-access-documents/src/lib/blog.api.ts b/nx-dev/data-access-documents/src/lib/blog.api.ts index 4dfd0469be..f1fd9eb33a 100644 --- a/nx-dev/data-access-documents/src/lib/blog.api.ts +++ b/nx-dev/data-access-documents/src/lib/blog.api.ts @@ -1,4 +1,4 @@ -import { readFileSync, readdirSync, accessSync, constants } from 'fs'; +import { readFileSync, accessSync, constants } from 'fs'; import { join, basename, parse, resolve } from 'path'; import { extractFrontmatter } from '@nx/nx-dev/ui-markdoc'; import { sortPosts } from './blog.util'; @@ -19,7 +19,6 @@ export class BlogApi { throw new Error('public blog root cannot be undefined'); } } - async getBlogTags(): Promise { const blogs = await this.getBlogs(); const tags = new Set(); @@ -28,8 +27,15 @@ export class BlogApi { }); return Array.from(tags); } + async getBlogs( + filterFn?: (post: BlogPostDataEntry) => boolean + ): Promise { + return await this.getAllBlogs(filterFn); + } - async getBlogs(): Promise { + async getAllBlogs( + filterFn?: (post: BlogPostDataEntry) => boolean + ): Promise { const files: string[] = await readdir(this.options.blogRoot); const authors = JSON.parse( readFileSync(join(this.options.blogRoot, 'authors.json'), 'utf8') @@ -63,65 +69,16 @@ export class BlogApi { filePath, slug, }; - if (!frontmatter.draft || process.env.NODE_ENV === 'development') { + const isDevelopment = process.env.NODE_ENV === 'development'; + const shouldIncludePost = !frontmatter.draft || isDevelopment; + + if (shouldIncludePost && (!filterFn || filterFn(post))) { allPosts.push(post); } } return sortPosts(allPosts); } - getBlogPosts(): BlogPostDataEntry[] { - const files: string[] = readdirSync(this.options.blogRoot); - const authors = JSON.parse( - readFileSync(join(this.options.blogRoot, 'authors.json'), 'utf8') - ); - const allPosts: BlogPostDataEntry[] = []; - - for (const file of files) { - const filePath = join(this.options.blogRoot, file); - // filter out directories (e.g. images) - if (!filePath.endsWith('.md')) continue; - - const content = readFileSync(filePath, 'utf8'); - const frontmatter = extractFrontmatter(content); - const slug = this.calculateSlug(filePath, frontmatter); - const { image, type } = this.determineOgImage(frontmatter.cover_image); - const post = { - content, - title: frontmatter.title ?? null, - description: frontmatter.description ?? null, - authors: authors.filter((author) => - frontmatter.authors.includes(author.name) - ), - date: this.calculateDate(file, frontmatter), - cover_image: frontmatter.cover_image - ? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser - : null, - tags: frontmatter.tags ?? [], - reposts: frontmatter.reposts ?? [], - pinned: frontmatter.pinned ?? false, - ogImage: image, - ogImageType: type, - filePath, - slug, - }; - - if (!frontmatter.draft || process.env.NODE_ENV === 'development') { - allPosts.push(post); - } - } - - return sortPosts(allPosts); - } - - getBlogPost(slug: string): BlogPostDataEntry { - const blogs = this.getBlogPosts(); - const blog = blogs.find((b) => b.slug === slug); - if (!blog) { - throw new Error(`Could not find blog post with slug: ${slug}`); - } - return blog; - } // Optimize this so we don't read the FS multiple times async getBlogPostBySlug( slug: string | null diff --git a/nx-dev/data-access-documents/src/lib/podcast.api.ts b/nx-dev/data-access-documents/src/lib/podcast.api.ts new file mode 100644 index 0000000000..0952154dc7 --- /dev/null +++ b/nx-dev/data-access-documents/src/lib/podcast.api.ts @@ -0,0 +1,16 @@ +import { BlogApi } from './blog.api'; +import { PodcastDataEntry } from './podcast.model'; + +export class PodcastApi { + _blogApi: BlogApi; + + constructor(options: { blogApi: BlogApi }) { + this._blogApi = options.blogApi; + } + + async getPodcastBlogs(): Promise { + return await this._blogApi.getBlogs((post) => + post.tags.map((t) => t.toLowerCase()).includes('podcast') + ); + } +} diff --git a/nx-dev/data-access-documents/src/lib/podcast.model.ts b/nx-dev/data-access-documents/src/lib/podcast.model.ts new file mode 100644 index 0000000000..1a9f44442a --- /dev/null +++ b/nx-dev/data-access-documents/src/lib/podcast.model.ts @@ -0,0 +1,5 @@ +import { BlogPostDataEntry } from './blog.model'; + +export interface PodcastDataEntry extends BlogPostDataEntry { + duration?: string; +} diff --git a/nx-dev/data-access-documents/src/node.index.ts b/nx-dev/data-access-documents/src/node.index.ts index c829d2e6a5..7534024a3d 100644 --- a/nx-dev/data-access-documents/src/node.index.ts +++ b/nx-dev/data-access-documents/src/node.index.ts @@ -4,3 +4,5 @@ export * from './lib/blog.util'; export * from './lib/blog.api'; export * from './lib/blog.model'; export * from './lib/tags.api'; +export * from './lib/podcast.model'; +export * from './lib/podcast.api'; diff --git a/nx-dev/nx-dev/app/blog/page.tsx b/nx-dev/nx-dev/app/blog/page.tsx index 9ab79c2337..dea815931d 100644 --- a/nx-dev/nx-dev/app/blog/page.tsx +++ b/nx-dev/nx-dev/app/blog/page.tsx @@ -26,7 +26,7 @@ export const metadata: Metadata = { }, }; async function getBlogs() { - return await blogApi.getBlogPosts(); + return await blogApi.getBlogs(); } async function getBlogTags() { diff --git a/nx-dev/nx-dev/app/podcast/page.tsx b/nx-dev/nx-dev/app/podcast/page.tsx new file mode 100644 index 0000000000..edcaa81c93 --- /dev/null +++ b/nx-dev/nx-dev/app/podcast/page.tsx @@ -0,0 +1,39 @@ +import { Metadata } from 'next'; +import { podcastApi } from '../../lib/podcast.api'; +import { DefaultLayout } from '@nx/nx-dev/ui-common'; +import { Hero, PodcastList } from '@nx/nx-dev/ui-podcast'; + +export const metadata: Metadata = { + title: 'Nx Podcast - Updates from the Nx & Nx Cloud team', + description: 'Latest podcasts from the Nx & Nx Cloud core team', + openGraph: { + url: 'https://nx.dev/podcast', + title: 'Nx Podcast - Updates from the Nx & Nx Cloud team', + description: + 'Stay updated with the latest podcasts 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: 'NxDev', + type: 'website', + }, +}; + +async function getPodcasts() { + return await podcastApi.getPodcastBlogs(); +} +export default async function Page() { + const podcasts = await getPodcasts(); + return ( + + + + + ); +} diff --git a/nx-dev/nx-dev/lib/podcast.api.ts b/nx-dev/nx-dev/lib/podcast.api.ts new file mode 100644 index 0000000000..dea27dbc7b --- /dev/null +++ b/nx-dev/nx-dev/lib/podcast.api.ts @@ -0,0 +1,4 @@ +import { blogApi } from './blog.api'; +import { PodcastApi } from '@nx/nx-dev/data-access-documents/node-only'; + +export const podcastApi = new PodcastApi({ blogApi }); diff --git a/nx-dev/nx-dev/public/images/podcast/podcast-hero.avif b/nx-dev/nx-dev/public/images/podcast/podcast-hero.avif new file mode 100644 index 0000000000..b863e4de48 Binary files /dev/null and b/nx-dev/nx-dev/public/images/podcast/podcast-hero.avif differ diff --git a/nx-dev/ui-common/src/lib/headers/menu-items.ts b/nx-dev/ui-common/src/lib/headers/menu-items.ts index 65499de13f..4ffff65af1 100644 --- a/nx-dev/ui-common/src/lib/headers/menu-items.ts +++ b/nx-dev/ui-common/src/lib/headers/menu-items.ts @@ -13,6 +13,8 @@ import { UserGroupIcon, ComputerDesktopIcon, GlobeAltIcon, + MicrophoneIcon, + VideoCameraIcon, } from '@heroicons/react/24/outline'; import { FC, SVGProps } from 'react'; import { DiscordIcon } from '../discord-icon'; @@ -172,6 +174,22 @@ export const learnItems: MenuItem[] = [ isNew: false, isHighlight: false, }, + { + name: 'Podcasts', + description: null, + href: '/podcast', + icon: MicrophoneIcon, + isNew: false, + isHighlight: false, + }, + { + name: 'Webinars', + description: null, + href: 'https://go.nx.dev/webinar', + icon: ComputerDesktopIcon, + isNew: false, + isHighlight: false, + }, { name: 'Video tutorials', description: null, @@ -215,10 +233,10 @@ export const eventItems: MenuItem[] = [ isHighlight: false, }, { - name: 'Webinars', + name: 'Live Streams', description: null, - href: 'https://go.nx.dev/webinar', - icon: ComputerDesktopIcon, + href: 'https://www.youtube.com/@nxdevtools/streams', + icon: VideoCameraIcon, isNew: false, isHighlight: false, }, diff --git a/nx-dev/ui-common/src/lib/headers/sections-menu.tsx b/nx-dev/ui-common/src/lib/headers/sections-menu.tsx index 5cbbcb73df..646dea834f 100644 --- a/nx-dev/ui-common/src/lib/headers/sections-menu.tsx +++ b/nx-dev/ui-common/src/lib/headers/sections-menu.tsx @@ -10,7 +10,7 @@ export function SectionsMenu({
{Object.keys(sections).map((section) => ( -
+
{section}
diff --git a/nx-dev/ui-icons/src/index.ts b/nx-dev/ui-icons/src/index.ts index 217df3dc7d..86209781a8 100644 --- a/nx-dev/ui-icons/src/index.ts +++ b/nx-dev/ui-icons/src/index.ts @@ -119,3 +119,9 @@ export * from './lib/technologies/vite'; export * from './lib/technologies/vitest'; export * from './lib/technologies/vue'; export * from './lib/technologies/webpack'; + +// PODCASTS +export * from './lib/podcasts/amazon-music'; +export * from './lib/podcasts/apple-podcasts'; +export * from './lib/podcasts/i-heart-radio'; +export * from './lib/podcasts/spotify'; diff --git a/nx-dev/ui-icons/src/lib/podcasts/amazon-music.tsx b/nx-dev/ui-icons/src/lib/podcasts/amazon-music.tsx new file mode 100644 index 0000000000..62e24e0952 --- /dev/null +++ b/nx-dev/ui-icons/src/lib/podcasts/amazon-music.tsx @@ -0,0 +1,18 @@ +import { FC, SVGProps } from 'react'; +/** + * Color: #46C3D0 + */ +export const AmazonMusicIcon: FC> = (props) => { + return ( + + Amazon Music + + + ); +}; diff --git a/nx-dev/ui-icons/src/lib/podcasts/apple-podcasts.tsx b/nx-dev/ui-icons/src/lib/podcasts/apple-podcasts.tsx new file mode 100644 index 0000000000..be24dbe168 --- /dev/null +++ b/nx-dev/ui-icons/src/lib/podcasts/apple-podcasts.tsx @@ -0,0 +1,20 @@ +import { FC, SVGProps } from 'react'; + +/** + * Color: #9933CC + */ + +export const ApplePodcastsIcon: FC> = (props) => { + return ( + + Apple Podcasts + + + ); +}; diff --git a/nx-dev/ui-icons/src/lib/podcasts/i-heart-radio.tsx b/nx-dev/ui-icons/src/lib/podcasts/i-heart-radio.tsx new file mode 100644 index 0000000000..5be59cf530 --- /dev/null +++ b/nx-dev/ui-icons/src/lib/podcasts/i-heart-radio.tsx @@ -0,0 +1,20 @@ +import { FC, SVGProps } from 'react'; + +/** + * Color: #C6002B + */ + +export const IHeartRadioIcon: FC> = (props) => { + return ( + + iHeartRadio + + + ); +}; diff --git a/nx-dev/ui-icons/src/lib/podcasts/spotify.tsx b/nx-dev/ui-icons/src/lib/podcasts/spotify.tsx new file mode 100644 index 0000000000..71987398f5 --- /dev/null +++ b/nx-dev/ui-icons/src/lib/podcasts/spotify.tsx @@ -0,0 +1,20 @@ +import { FC, SVGProps } from 'react'; + +/** + * Color: #1DB954 + */ + +export const SpotifyIcon: FC> = (props) => { + return ( + + Spotify + + + ); +}; diff --git a/nx-dev/ui-podcast/.babelrc b/nx-dev/ui-podcast/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/nx-dev/ui-podcast/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/nx-dev/ui-podcast/.eslintrc.json b/nx-dev/ui-podcast/.eslintrc.json new file mode 100644 index 0000000000..a39ac5d057 --- /dev/null +++ b/nx-dev/ui-podcast/.eslintrc.json @@ -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": {} + } + ] +} diff --git a/nx-dev/ui-podcast/README.md b/nx-dev/ui-podcast/README.md new file mode 100644 index 0000000000..f687f62b87 --- /dev/null +++ b/nx-dev/ui-podcast/README.md @@ -0,0 +1,7 @@ +# ui-podcast + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-podcast` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/nx-dev/ui-podcast/project.json b/nx-dev/ui-podcast/project.json new file mode 100644 index 0000000000..fd32f08b2a --- /dev/null +++ b/nx-dev/ui-podcast/project.json @@ -0,0 +1,9 @@ +{ + "name": "ui-podcast", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "nx-dev/ui-podcast/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project ui-podcast --web", + "targets": {} +} diff --git a/nx-dev/ui-podcast/src/index.ts b/nx-dev/ui-podcast/src/index.ts new file mode 100644 index 0000000000..d961e66414 --- /dev/null +++ b/nx-dev/ui-podcast/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/hero'; +export * from './lib/podcast-list'; diff --git a/nx-dev/ui-podcast/src/lib/hero.tsx b/nx-dev/ui-podcast/src/lib/hero.tsx new file mode 100644 index 0000000000..9ecbaf7845 --- /dev/null +++ b/nx-dev/ui-podcast/src/lib/hero.tsx @@ -0,0 +1,35 @@ +import { SectionHeading } from '@nx/nx-dev/ui-common'; +import { ListenOn } from './listen-on'; + +export function Hero(): JSX.Element { + return ( +
+
+
+ + The Enterprise Software Podcast + +
+ + Listen in to some exciting conversations with the hidden + architects of enterprise software. + +
+

+ Available On:{' '} +

+ +
+
+
+
+ Illustration of a microphone +
+
+
+ ); +} diff --git a/nx-dev/ui-podcast/src/lib/listen-on.tsx b/nx-dev/ui-podcast/src/lib/listen-on.tsx new file mode 100644 index 0000000000..da771c7736 --- /dev/null +++ b/nx-dev/ui-podcast/src/lib/listen-on.tsx @@ -0,0 +1,53 @@ +import { + AmazonMusicIcon, + ApplePodcastsIcon, + IHeartRadioIcon, + SpotifyIcon, +} from '@nx/nx-dev/ui-icons'; + +export function ListenOn(): JSX.Element { + const platforms = [ + { + name: 'Amazon Music', + url: 'https://music.amazon.com/podcasts/a221fdad-36fd-4695-a5b4-038d7b99d284/the-enterprise-software-podcast-by-nx', + icon: AmazonMusicIcon, + }, + { + name: 'Apple Podcasts', + url: 'https://podcasts.apple.com/us/podcast/the-enterprise-software-podcast-by-nx/id1752704996', + icon: ApplePodcastsIcon, + }, + { + name: 'iHeartRadio', + url: 'https://www.iheart.com/podcast/269-the-enterprise-software-po-186891508/', + icon: IHeartRadioIcon, + }, + { + name: 'Spotify', + url: 'https://open.spotify.com/show/6Axjn4Qh7PUWlGbNqzE7J4', + icon: SpotifyIcon, + }, + ]; + + return ( +
    + {platforms.map((platform) => { + return ( +
  • + + + +
  • + ); + })} +
+ ); +} diff --git a/nx-dev/ui-podcast/src/lib/podcast-list-item.tsx b/nx-dev/ui-podcast/src/lib/podcast-list-item.tsx new file mode 100644 index 0000000000..377c61d6bf --- /dev/null +++ b/nx-dev/ui-podcast/src/lib/podcast-list-item.tsx @@ -0,0 +1,33 @@ +import { BlogAuthors } from '@nx/nx-dev/ui-blog'; +import Link from 'next/link'; +import type { PodcastDataEntry } from '@nx/nx-dev/data-access-documents/node-only'; + +export interface PodcastListItemProps { + podcast: PodcastDataEntry; + episode: number; +} +export function PodcastListItem({ podcast, episode }: PodcastListItemProps) { + const formattedDate = new Date(podcast.date).toLocaleDateString('en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + }); + return ( + + + Episode {episode}: {podcast.title} + + + + + + + + + ); +} diff --git a/nx-dev/ui-podcast/src/lib/podcast-list.tsx b/nx-dev/ui-podcast/src/lib/podcast-list.tsx new file mode 100644 index 0000000000..047fda6396 --- /dev/null +++ b/nx-dev/ui-podcast/src/lib/podcast-list.tsx @@ -0,0 +1,27 @@ +import { PodcastDataEntry } from '@nx/nx-dev/data-access-documents/node-only'; +import { PodcastListItem } from './podcast-list-item'; + +export interface PodcastListProps { + podcasts: PodcastDataEntry[]; +} + +export function PodcastList({ podcasts }: PodcastListProps): JSX.Element { + return podcasts.length < 1 ? ( +
+

+ No podcasts as yet but stay tuned! +

+
+ ) : ( +
+
+

Podcasts

+
+
+ {podcasts?.map((post, index) => ( + + ))} +
+
+ ); +} diff --git a/nx-dev/ui-podcast/tsconfig.json b/nx-dev/ui-podcast/tsconfig.json new file mode 100644 index 0000000000..95cfeb243d --- /dev/null +++ b/nx-dev/ui-podcast/tsconfig.json @@ -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" +} diff --git a/nx-dev/ui-podcast/tsconfig.lib.json b/nx-dev/ui-podcast/tsconfig.lib.json new file mode 100644 index 0000000000..cfc4843293 --- /dev/null +++ b/nx-dev/ui-podcast/tsconfig.lib.json @@ -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"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index e93b67d768..007ac4e0bb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -108,6 +108,7 @@ "@nx/nx-dev/ui-icons": ["nx-dev/ui-icons/src/index.ts"], "@nx/nx-dev/ui-markdoc": ["nx-dev/ui-markdoc/src/index.ts"], "@nx/nx-dev/ui-member-card": ["nx-dev/ui-member-card/src/index.ts"], + "@nx/nx-dev/ui-podcast": ["nx-dev/ui-podcast/src/index.ts"], "@nx/nx-dev/ui-pricing": ["nx-dev/ui-pricing/src/index.ts"], "@nx/nx-dev/ui-primitives": ["nx-dev/ui-primitives/src/index.ts"], "@nx/nx-dev/ui-references": ["nx-dev/ui-references/src/index.ts"],