feat(nx-dev): Use app router for blogs (#23127)

The PR activates the app router for the Blog page at /blog. 

Its purpose is to test Next.js changes within nx-dev, allowing us to
identify and address any issues that users might encounter.
Integrating these changes into our environment, we can gain firsthand
experience and insights into potential problems, ensuring that the
updates are robust and reliable.

This approach helps us improve the overall quality and user experience
of our platform by proactively identifying and resolving any issues that
could affect consumers.
This commit is contained in:
Nicholas Cunningham 2024-06-11 07:28:29 -06:00 committed by GitHub
parent 8351cb11f3
commit b9b89b2575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 412 additions and 401 deletions

View File

@ -1,3 +1,4 @@
'use client';
/* eslint-disable @nx/enforce-module-boundaries */
/* nx-ignore-next-line */
import type {

View File

@ -1,3 +1,4 @@
'use client';
/* eslint-disable @nx/enforce-module-boundaries */
/* nx-ignore-next-line */
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';

View File

@ -3,6 +3,7 @@ import { join, basename } from 'path';
import { extractFrontmatter } from '@nx/nx-dev/ui-markdoc';
import { sortPosts } from './blog.util';
import { BlogPostDataEntry } from './blog.model';
import { readFile, readdir } from 'fs/promises';
export class BlogApi {
constructor(
@ -19,6 +20,43 @@ export class BlogApi {
}
}
async getBlogs(): Promise<BlogPostDataEntry[]> {
const files: string[] = await readdir(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);
if (!filePath.endsWith('.md')) continue;
const content = await readFile(filePath, 'utf8');
const frontmatter = extractFrontmatter(content);
const slug = this.calculateSlug(filePath, frontmatter);
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,
filePath,
slug,
};
if (!frontmatter.draft || process.env.NODE_ENV === 'development') {
allPosts.push(post);
}
}
return sortPosts(allPosts);
}
getBlogPosts(): BlogPostDataEntry[] {
const files: string[] = readdirSync(this.options.blogRoot);
const authors = JSON.parse(
@ -68,6 +106,13 @@ export class BlogApi {
}
return blog;
}
// Optimize this so we don't read the FS multiple times
async getBlogPostBySlug(
slug: string | null
): Promise<BlogPostDataEntry | undefined> {
if (!slug) throw new Error(`Could not find blog post with slug: ${slug}`);
return (await this.getBlogs()).find((post) => post.slug === slug);
}
private calculateSlug(filePath: string, frontmatter: any): string {
const baseName = basename(filePath, '.md');

View File

@ -6,7 +6,7 @@ import {
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
import Head from 'next/head';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useRouter } from 'next/navigation';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';

View File

@ -0,0 +1,18 @@
'use client';
import { sendPageViewEvent } from '@nx/nx-dev/feature-analytics';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function AppRouterAnalytics({ gaMeasurementId }) {
const pathName = usePathname();
const [lastPath, setLastPath] = useState(pathName);
useEffect(() => {
if (pathName !== lastPath) {
setLastPath(pathName);
sendPageViewEvent({ gaId: gaMeasurementId, path: pathName });
}
}, [pathName, gaMeasurementId, lastPath]);
return <></>;
}

View File

@ -0,0 +1,54 @@
import type { Metadata, ResolvingMetadata } from 'next';
import { blogApi } from '../../../lib/blog.api';
import { BlogDetails } from '@nx/nx-dev/ui-blog';
interface BlogPostDetailProps {
params: { slug: string };
}
export async function generateMetadata(
{ params: { slug } }: BlogPostDetailProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const post = await blogApi.getBlogPostBySlug(slug);
const previousImages = (await parent).openGraph?.images ?? [];
return {
title: `${post.title} | Nx Blog`,
description: 'Latest news from the Nx & Nx Cloud core team',
openGraph: {
url: `https://nx.dev/blog/${slug}`,
title: post.title,
description: post.description,
images: [
{
url: post.cover_image
? `https://nx.dev${post.cover_image}`
: 'https://nx.dev/socials/nx-media.png',
width: 800,
height: 421,
alt: 'Nx: Smart, Fast and Extensible Build System',
type: 'image/jpeg',
},
...previousImages,
],
},
};
}
export async function generateStaticParams() {
return (await blogApi.getBlogs()).map((post) => {
return { slug: post.slug };
});
}
export default async function BlogPostDetail({
params: { slug },
}: BlogPostDetailProps) {
const blog = await blogApi.getBlogPostBySlug(slug);
return blog ? (
<>
{/* This empty div is necessary as app router does not automatically scroll on route changes */}
<div></div>
<BlogDetails post={blog} />
</>
) : null;
}

View File

@ -0,0 +1,33 @@
import type { Metadata } from 'next';
import { blogApi } from '../../lib/blog.api';
import { BlogContainer } from '@nx/nx-dev/ui-blog';
export const metadata: Metadata = {
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
description: 'Latest news from the Nx & Nx Cloud core team',
openGraph: {
url: 'https://nx.dev/blog',
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
description:
'Stay updated with the latest news, articles, and updates 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 getBlogs() {
return await blogApi.getBlogPosts();
}
export default async function BlogIndex() {
const blogs = await getBlogs();
return <BlogContainer blogPosts={blogs} />;
}

View File

@ -0,0 +1,77 @@
import Script from 'next/script';
export default function GlobalScripts({ gaMeasurementId }) {
return (
<>
<Script
id="gtag-script-dependency"
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gaMeasurementId}`}
/>
<Script
id="gtag-script-loader"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '${gaMeasurementId}', {
page_path: window.location.pathname,
});
`,
}}
/>
{/* Apollo.io Embed Code */}
<Script
type="text/javascript"
id="apollo-script-loader"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `function initApollo(){var n=Math.random().toString(36).substring(7),o=document.createElement("script"); o.src="https://assets.apollo.io/micro/website-tracker/tracker.iife.js?nocache="+n,o.async=!0,o.defer=!0,o.onload=function(){window.trackingFunctions.onLoad({appId:"65e1db2f1976f30300fd8b26"})},document.head.appendChild(o)}initApollo();`,
}}
/>
{/* HubSpot Analytics */}
<Script
id="hs-script-loader"
strategy="afterInteractive"
src="https://js.hs-scripts.com/2757427.js"
/>
{/* HubSpot FORMS Embed Code */}
<Script
type="text/javascript"
id="hs-forms-script-loader"
strategy="afterInteractive"
src="//js.hsforms.net/forms/v2.js"
/>
{/* Hotjar Analytics */}
<Script
id="hotjar-script-loader"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:2774127,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,
}}
/>
<Script
id="twitter-campain-pixelcode"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
},s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
twq('config','obtp4');
`,
}}
/>
</>
);
}

View File

@ -0,0 +1,91 @@
import type { Metadata, Viewport } from 'next';
import { Header, Footer, AnnouncementBanner } from '@nx/nx-dev/ui-common';
import AppRouterAnalytics from './app-router-analytics';
import GlobalScripts from './global-scripts';
import '../styles/main.css';
// Metadata for the entire site
export const metadata: Metadata = {
appleWebApp: { title: 'Nx' },
applicationName: 'Nx',
icons: [
{
url: '/favicon/favicon.svg',
type: 'image/svg+xml',
rel: 'icon',
},
{
url: '/favicon/favicon-32x32.png',
sizes: '32x32',
type: 'image/png',
rel: 'icon',
},
{
url: '/favicon/favicon.ico',
type: 'image/x-icon',
rel: 'icon',
},
{
url: '/favicon/apple-touch-icon.png',
sizes: '180x180',
rel: 'apple-touch-icon',
},
{
url: '/favicon/safari-pinned-tab.svg',
color: '#5bbad5',
rel: 'mask-icon',
},
],
};
// Viewport settings for the entire site
export const viewport: Viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#F8FAFC' },
{ media: '(prefers-color-scheme: dark)', color: '#0F172A' },
],
width: 'device-width',
initialScale: 1,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const gaMeasurementId = 'UA-88380372-10';
return (
<html lang="en" className="h-full scroll-smooth" suppressHydrationWarning>
<AppRouterAnalytics gaMeasurementId={gaMeasurementId} />
<head>
<meta
name="msapplication-TileColor"
content="#DA532C"
key="windows-tile-color"
/>
<script
type="text/javascript"
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
} catch (_) {}
`,
}}
/>
</head>
<body className="h-full bg-white text-slate-700 antialiased selection:bg-blue-500 selection:text-white dark:bg-slate-900 dark:text-slate-400 dark:selection:bg-sky-500">
<AnnouncementBanner />
<Header />
{children}
<Footer />
<GlobalScripts gaMeasurementId={gaMeasurementId} />
</body>
</html>
);
}

View File

@ -1,11 +0,0 @@
import { DocumentsApi } from '@nx/nx-dev/data-access-documents/node-only';
import documents from '../public/documentation/generated/manifests/recipes.json';
import { tagsApi } from './tags.api';
export const nxRecipesApi = new DocumentsApi({
id: 'recipes',
manifest: documents,
prefix: '',
publicDocsRoot: 'public/documentation',
tagsApi,
});

View File

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -3,6 +3,10 @@ const { withNx } = require('@nx/next/plugins/with-nx');
const redirectRules = require('./redirect-rules');
module.exports = withNx({
// Disable the type checking for now, we need to resolve the issues first.
typescript: {
ignoreBuildErrors: true,
},
// For both client and server
env: {
VERCEL: process.env.VERCEL,

View File

@ -1,69 +0,0 @@
import { GetStaticProps, GetStaticPaths } from 'next';
import { blogApi } from '../../lib/blog.api';
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
import { NextSeo } from 'next-seo';
import { Footer, Header } from '@nx/nx-dev/ui-common';
import { BlogDetails } from '@nx/nx-dev/ui-blog';
interface BlogPostDetailProps {
post: BlogPostDataEntry;
}
export default function BlogPostDetail({ post }: BlogPostDetailProps) {
return (
<>
<NextSeo
title={`${post.title} | Nx Blog`}
description="Latest news from the Nx & Nx Cloud core team"
openGraph={{
url: 'https://nx.dev', // + router.asPath,
title: post.title,
description: post.description,
images: [
{
url: post.cover_image
? `https://nx.dev${post.cover_image}`
: 'https://nx.dev/socials/nx-media.png',
width: 800,
height: 421,
alt: 'Nx: Smart, Fast and Extensible Build System',
type: 'image/jpeg',
},
],
siteName: 'NxDev',
type: 'website',
}}
/>
<Header />
<BlogDetails post={post} />
<Footer />
</>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
// optimize s.t. we don't read the FS multiple times; for now it's ok
// const posts = await blogApi.getBlogPosts();
// const post = posts.find((p) => p.slug === context.params?.slug);
try {
const post = await blogApi.getBlogPost(context.params?.slug as string);
return { props: { post } };
} catch (e) {
return {
notFound: true,
props: {
statusCode: 404,
},
};
}
};
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await blogApi.getBlogPosts();
const paths = posts.map((post) => ({
params: { slug: post.slug },
}));
return { paths, fallback: 'blocking' };
};

View File

@ -1,53 +0,0 @@
import { Footer, Header } from '@nx/nx-dev/ui-common';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
import { blogApi } from '../../lib/blog.api';
import { BlogContainer } from '@nx/nx-dev/ui-blog';
interface BlogListProps {
blogposts: BlogPostDataEntry[];
}
export function getStaticProps(): { props: BlogListProps } {
const blogposts = blogApi.getBlogPosts();
return {
props: {
blogposts,
},
};
}
export default function Blog({ blogposts }: BlogListProps): JSX.Element {
const router = useRouter();
return (
<>
<NextSeo
title="Nx Blog - Updates from the Nx & Nx Cloud team"
description="Latest news from the Nx & Nx Cloud core team"
openGraph={{
url: 'https://nx.dev' + router.asPath,
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
description:
'Stay updated with the latest news, articles, and updates 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',
}}
/>
<Header />
<BlogContainer blogPosts={blogposts} />
<Footer />
</>
);
}

View File

@ -70,7 +70,11 @@ export default function RspackConfigSetup({
role="main"
className="flex h-full flex-1 overflow-y-hidden"
>
<SidebarContainer menu={vm.menu} navIsOpen={navIsOpen} />
<SidebarContainer
menu={vm.menu}
navIsOpen={navIsOpen}
toggleNav={toggleNav}
/>
<div
ref={wrapperElement}
id="wrapper"

View File

@ -31,7 +31,7 @@ module.exports = {
mode: 'jit',
darkMode: 'class',
content: [
path.join(__dirname, 'pages/**/*.{js,ts,jsx,tsx}'),
path.join(__dirname, '{pages,app}/**/*.{js,ts,jsx,tsx}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {

View File

@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node", "jest"],
"lib": ["dom", "es2019"]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts"
],
"include": ["**/*.js", "**/*.ts", "**/*.tsx", "next-env.d.ts"]
}

View File

@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"allowJs": true,
"esModuleInterop": true,
@ -10,18 +11,34 @@
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strictNullChecks": true,
"incremental": true
"incremental": true,
"strictNullChecks": false,
"plugins": [
{
"name": "next"
}
]
},
"files": [],
"include": [],
"types": ["node", "jest"],
"include": [
"**/*.js",
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
".next/types/**/*.ts",
"../../dist/nx-dev/nx-dev/.next/types/**/*.ts"
],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"exclude": ["node_modules"]
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"jest.config.ts",
"node_modules"
]
}

View File

@ -1,6 +1,6 @@
export { BlogAuthors } from './lib/authors';
export { BlogEntry, BlogEntryProps } from './lib/blog-entry';
export { MoreBlogs, MoreBlogsProps } from './lib/more-blogs';
export { BlogDetails, BlogDetailsProps } from './lib/blog-details';
export { BlogEntry } from './lib/blog-entry';
export { MoreBlogs } from './lib/more-blogs';
export { BlogDetails } from './lib/blog-details';
export { BlogContainer } from './lib/blog-container';
export { FeaturedBlogs } from './lib/featured-blogs';

View File

@ -4,8 +4,10 @@ import type { BlogAuthor } from '@nx/nx-dev/data-access-documents/node-only';
export function BlogAuthors({
authors,
showAuthorDetails = true,
}: {
authors: BlogAuthor[];
showAuthorDetails?: boolean;
}): JSX.Element {
return (
<div className="relative isolate flex items-center -space-x-2">
@ -21,7 +23,7 @@ export function BlogAuthors({
src={`/documentation/blog/images/authors/${author.name}.jpeg`}
className="relative inline-block h-6 w-6 rounded-full ring-1 ring-white grayscale dark:ring-slate-900"
/>
<AuthorDetail author={author} />
{showAuthorDetails && <AuthorDetail author={author} />}
</div>
))}
</div>

View File

@ -8,6 +8,27 @@ import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
export interface BlogDetailsProps {
post: BlogPostDataEntry;
}
export async function generateMetadata({ post }: BlogDetailsProps) {
return {
title: post.title,
description: post.description,
openGraph: {
images: [
{
url: post.cover_image
? `https://nx.dev${post.cover_image}`
: 'https://nx.dev/socials/nx-media.png',
width: 800,
height: 421,
alt: 'Nx: Smart, Fast and Extensible Build System',
type: 'image/jpeg',
},
],
},
};
}
export function BlogDetails({ post }: BlogDetailsProps) {
const { node } = renderMarkdown(post.content, {
filePath: post.filePath ?? '',

View File

@ -41,7 +41,7 @@ export function MoreBlogs({ blogs }: MoreBlogsProps) {
{formattedDate}
</span>
<span className="hidden flex-1 overflow-hidden sm:inline-block">
<BlogAuthors authors={post.authors} />
<BlogAuthors authors={post.authors} showAuthorDetails={false} />
</span>
</Link>
);

View File

@ -19,7 +19,6 @@ export * from './lib/tweet';
export * from './lib/typography';
export * from './lib/github-star-widget';
export * from './lib/youtube.component';
export * from './lib/image-theme';
export * from './lib/twitter-icon';
export * from './lib/discord-icon';
export { resourceMenuItems } from './lib/headers/menu-items';

View File

@ -1,3 +1,4 @@
'use client';
import cx from 'classnames';
import { ReactNode, createContext, useState } from 'react';

View File

@ -1,3 +1,4 @@
'use client';
import { Fragment, type JSX } from 'react';
import {
ArrowUpRightIcon,

View File

@ -1,3 +1,4 @@
'use client';
import { Dialog, Disclosure, Popover, Transition } from '@headlessui/react';
import {
ArrowUpRightIcon,
@ -7,7 +8,6 @@ import {
} from '@heroicons/react/24/outline';
import cx from 'classnames';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, useEffect, useState } from 'react';
import { ButtonLink } from '../button';
import {
@ -28,7 +28,6 @@ import { NxCloudIcon } from '../nx-cloud-icon';
export function Header(): JSX.Element {
let [isOpen, setIsOpen] = useState(false);
const router = useRouter();
// We need to close the popover if the route changes or the window is resized to prevent the popover from being stuck open.
const checkSizeAndClosePopover = () => {

View File

@ -1,46 +0,0 @@
import Image from 'next/image';
import { useEffect, useState } from 'react';
export interface ImageThemeProps {
lightSrc: string;
darkSrc: string;
alt?: string;
[key: string]: any;
}
export function ImageTheme({
lightSrc,
darkSrc,
alt,
...props
}: ImageThemeProps) {
const [src, setSrc] = useState(lightSrc);
// Listen for theme change and update the image
useEffect(() => {
const updateImageSource = () => {
const isDarkMode = document.documentElement.classList.contains('dark');
setSrc(isDarkMode ? darkSrc : lightSrc);
};
updateImageSource();
// Event listener for changes in system theme
const themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
themeMediaQuery.addEventListener('change', updateImageSource);
const observer = new MutationObserver(updateImageSource);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
// Cleanup function to remove event listener and observer on component unmount
return () => {
themeMediaQuery.removeEventListener('change', updateImageSource);
observer.disconnect();
};
}, []);
return <Image src={src} alt={alt ?? ''} {...props} />;
}

View File

@ -1,3 +1,4 @@
'use client';
import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
import { Sidebar, SidebarMobile } from './sidebar';
import { useMemo } from 'react';

View File

@ -1,3 +1,4 @@
'use client';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { AlgoliaSearch } from '@nx/nx-dev/feature-search';

View File

@ -1,3 +1,3 @@
export { Fence, FenceProps } from './lib/fence';
export { Fence, type FenceProps } from './lib/fence';
export { TerminalOutput } from './lib/fences/terminal-output';
export { TerminalShellWrapper } from './lib/fences/terminal-shell';

View File

@ -1,3 +1,4 @@
'use client';
import {
ClipboardDocumentCheckIcon,
ClipboardDocumentIcon,

View File

@ -1,9 +1,9 @@
'use client';
import { Fence, FenceProps } from '@nx/nx-dev/ui-fence';
import { useRouter } from 'next/router';
import { useRouter, usePathname, useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
const useUrlHash = (initialValue: string) => {
const router = useRouter();
const [hash, setHash] = useState(initialValue);
const updateHash = (str: string) => {
@ -11,34 +11,28 @@ const useUrlHash = (initialValue: string) => {
setHash(str.split('#')[1]);
};
useEffect(() => {
const onWindowHashChange = () => updateHash(window.location.hash);
const onNextJSHashChange = (url: string) => updateHash(url);
const params = useParams();
router.events.on('hashChangeStart', onNextJSHashChange);
window.addEventListener('hashchange', onWindowHashChange);
window.addEventListener('load', onWindowHashChange);
return () => {
router.events.off('hashChangeStart', onNextJSHashChange);
window.removeEventListener('load', onWindowHashChange);
window.removeEventListener('hashchange', onWindowHashChange);
};
}, [router.asPath, router.events]);
useEffect(() => {
updateHash(window.location.hash);
}, [params]);
return hash;
};
export function FenceWrapper(props: FenceProps) {
const { push, asPath } = useRouter();
const { push } = useRouter();
const pathName = usePathname();
const hash = decodeURIComponent(useUrlHash(''));
const modifiedProps: FenceProps = {
...props,
selectedLineGroup: hash,
onLineGroupSelectionChange: (selection: string) => {
push(asPath.split('#')[0] + '#' + selection);
push(pathName.split('#')[0] + '#' + selection);
},
};
return (
<div className="my-8 w-full">
<Fence {...modifiedProps} />

View File

@ -1,3 +1,5 @@
'use client';
import {
ChevronDoubleUpIcon,
ChevronDoubleDownIcon,

View File

@ -1,3 +1,4 @@
'use client';
import { useTheme } from '@nx/nx-dev/ui-theme';
import dynamic from 'next/dynamic';
import { ReactElement, useEffect, useState } from 'react';

View File

@ -1,3 +1,4 @@
'use client';
import { JSX, ReactElement, useEffect, useState } from 'react';
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph/ui-project-details';
import { ExpandedTargetsProvider } from '@nx/graph/shared';

View File

@ -1,3 +1,4 @@
'use client';
import {
createContext,
ReactNode,

View File

@ -1,3 +1,4 @@
'use client';
// TODO@ben: refactor to use HeadlessUI tabs
import cx from 'classnames';
import {

View File

@ -1,3 +1,4 @@
'use client';
import { useEffect, useRef } from 'react';
export function VideoLoop({

View File

@ -1,3 +1,4 @@
'use client';
import { useLayoutEffect as ReactUseLayoutEffect } from 'react';
/**

View File

@ -1,4 +1 @@
export * from './lib/references-nav-list';
export * from './lib/references-index-item';
export * from './lib/references-section';
export * from './lib/icons-map';

View File

@ -1,30 +0,0 @@
import cx from 'classnames';
import { iconsMap } from './icons-map';
export function ReferencesIndexItem(pkg: {
active: string;
callback: (id: any) => any;
id: string;
name: string;
path: string;
}): JSX.Element {
return (
<button
onClick={() => pkg.callback(pkg.id)}
className={cx(
'group relative flex items-center justify-center gap-3 overflow-hidden rounded-lg border border-slate-200 px-3 py-2 text-slate-700 transition hover:bg-slate-50',
pkg.active === pkg.id ? 'bg-slate-200' : ''
)}
>
<img
className="h-5 w-5 object-cover opacity-75"
loading="lazy"
src={iconsMap[pkg.id]}
alt={pkg.name + ' illustration'}
aria-hidden="true"
/>
<span className="text-base font-medium">{pkg.name}</span>
</button>
);
}

View File

@ -1,35 +0,0 @@
import { ChevronRightIcon } from '@heroicons/react/24/outline';
import { MenuItem } from '@nx/nx-dev/models-menu';
export function ReferencesNavList({
header,
links,
}: {
header: {
title: string;
icon: JSX.Element;
};
links: MenuItem;
}): JSX.Element {
return (
<nav className="relative">
<h4 className="mb-5 flex items-center text-lg font-bold tracking-tight text-slate-700 lg:text-xl">
{header.icon}
{header.title}
</h4>
<ul className="space-y-0.5 text-sm">
{links.itemList?.map((item, subIndex) => (
<li key={[item.id, subIndex].join('-')}>
<a
href={item.path}
className="group block flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50/10 px-4 py-2 text-slate-500 transition-all hover:bg-slate-100 hover:text-slate-900"
>
{item.name}
<ChevronRightIcon className="h-4 w-4 transition-all group-hover:translate-x-2" />
</a>
</li>
))}
</ul>
</nav>
);
}

View File

@ -1,26 +0,0 @@
import { iconsMap } from './icons-map';
export function ReferencesPackageCard(pkg: {
id: string;
name: string;
path: string;
}): JSX.Element {
return (
<div className="flex items-center gap-4">
<div className="ml-3 block flex-shrink-0">
<img
className="h-10 w-10 rounded-lg object-cover opacity-75"
loading="lazy"
src={iconsMap[pkg.id]}
alt={pkg.name + ' illustration'}
aria-hidden="true"
/>
</div>
<div>
<h3 className="text-2xl font-bold text-slate-900 lg:text-3xl">
{pkg.name}
</h3>
</div>
</div>
);
}

View File

@ -1,76 +0,0 @@
import {
BookmarkIcon,
CogIcon,
CommandLineIcon,
CpuChipIcon,
} from '@heroicons/react/24/solid';
import { MenuItem } from '@nx/nx-dev/models-menu';
import { ReferencesNavList } from './references-nav-list';
import { ReferencesPackageCard } from './references-package-card';
export function ReferencesSection({
section,
}: {
section: MenuItem;
}): JSX.Element {
const guides: MenuItem | null =
section.itemList?.find((x) => x.id === section.id + '-guides') || null;
const executors =
section.itemList?.find((x) => x.id === section.id + '-executors') || null;
const generators =
section.itemList?.find((x) => x.id === section.id + '-generators') || null;
return (
<section
id={section.id}
className="relative py-2 md:grid md:grid-cols-3 md:gap-x-16"
>
<header className="md:col-span-1">
<ReferencesPackageCard
id={section.id}
name={section.name as string}
path={section.path as string}
/>
</header>
<div className="mt-10 grid grid-cols-2 gap-8 sm:grid-cols-3 sm:space-y-0 md:col-span-2 md:mt-0">
{!!guides && (
<ReferencesNavList
header={{
icon:
section.id === 'nx' ? (
<CommandLineIcon
className="mr-2 h-5 w-5"
aria-hidden="true"
/>
) : (
<BookmarkIcon className="mr-2 h-5 w-5" aria-hidden="true" />
),
title: section.id === 'nx' ? 'Commands' : 'Guides',
}}
links={guides}
/>
)}
{!!executors && (
<ReferencesNavList
header={{
icon: <CpuChipIcon className="mr-2 h-5 w-5" aria-hidden="true" />,
title: executors.name,
}}
links={executors}
/>
)}
{!!generators && (
<ReferencesNavList
header={{
icon: <CogIcon className="mr-2 h-5 w-5" aria-hidden="true" />,
title: generators.name,
}}
links={generators}
/>
)}
</div>
</section>
);
}

View File

@ -1,3 +1,4 @@
'use client';
import { Listbox, Transition } from '@headlessui/react';
import {
ComputerDesktopIcon,

View File

@ -1,3 +1,4 @@
'use client';
import { useMemo, useSyncExternalStore } from 'react';
export type Theme = 'light' | 'dark' | 'system';