Skip to content

Commit

Permalink
Add right sidebar content for prismic content (#2795)
Browse files Browse the repository at this point in the history
* Add right sidebar content for prismic content

* Use slugify package

* Add comments

* Fix useEffect listener

* Only add anchor for heading1 and heading2

* Code refactor

* Add tests for right sidebar contents anchors

* Remove useEffect

* Move tests to navigation
  • Loading branch information
clari182 authored Nov 5, 2024
1 parent 389f4ac commit df12e56
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 32 deletions.
44 changes: 44 additions & 0 deletions site/gatsby-site/playwright/e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,48 @@ test.describe('Navigation', () => {
await page.getByTestId('sidebar-desktop').locator('#sidebar > [data-testid="sidebar-tree"] > [data-testid="sidebar-welcome"] > [data-testid="sidebar-link"]').click();
await expect(page.locator('.rightSideTitle')).not.toBeVisible();
});

test('Check right sidebar "Contents" scroll-to section on click on docs', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });

await page.goto('/');

const aboutLink = await page.locator('#main-footer .tw-footer-link').filter({ hasText: /^About$/ });
if (await aboutLink.count() > 0) {
await aboutLink.click();

await page.locator('.rightSideTitle:has-text("CONTENTS")').waitFor({ state: 'visible' });
const listItems = await page.locator('.rightSideBarUL li');
expect(await listItems.count()).toBeGreaterThanOrEqual(1);

await listItems.nth(1).click();
await page.waitForTimeout(700);

const subject = await page.locator('h2:has-text(\'Why "AI Incidents"?\')');
const boundingBox = await subject.boundingBox();
expect(boundingBox?.y).toBeCloseTo(0, 30);
}
});

test('Check right sidebar "Contents" scroll-to section on click on prismic blog post', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });

await page.goto('/blog');

const postLink = await page.locator('h5:has-text("AI Incident Roundup – January ‘24")');
if (await postLink.count() > 0) {
await postLink.click();

await page.locator('.rightSideTitle:has-text("CONTENTS")').waitFor({ state: 'visible' });
const listItems = await page.locator('.rightSideBarUL li');
expect(await listItems.count()).toBeGreaterThanOrEqual(1);

await listItems.nth(1).click();
await page.waitForTimeout(700);

const subject = await page.locator('h2:has-text("🗄 Trending in the AIID")');
const boundingBox = await subject.boundingBox();
expect(boundingBox?.y).toBeCloseTo(0, 30);
}
});
});
33 changes: 33 additions & 0 deletions site/gatsby-site/src/components/CustomHeaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react';
import slugify from 'slugify';

const getText = (children) => {
// Recursively find all text nodes in the children
let text = '';

React.Children.forEach(children, (child) => {
if (typeof child === 'string') {
text += child;
} else if (child.props && child.props.children) {
text += getText(child.props.children);
}
});
return text;
};

const Heading = ({ level, children }) => {
const text = getText(children);

const id = slugify(text, { lower: true });

const HeadingComponent = `h${level}`;

return <HeadingComponent id={id}>{children}</HeadingComponent>;
};

export const Heading1 = ({ children }) => <Heading level={1}>{children}</Heading>;
export const Heading2 = ({ children }) => <Heading level={2}>{children}</Heading>;
export const Heading3 = ({ children }) => <Heading level={3}>{children}</Heading>;
export const Heading4 = ({ children }) => <Heading level={4}>{children}</Heading>;
export const Heading5 = ({ children }) => <Heading level={5}>{children}</Heading>;
export const Heading6 = ({ children }) => <Heading level={6}>{children}</Heading>;
35 changes: 35 additions & 0 deletions site/gatsby-site/src/components/PrismicOutline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

const PrismicSidebarLayout = ({ tableOfContents }) => {
let navItems;

if (tableOfContents.length > 0) {
navItems = tableOfContents.map((item) => {
return (
<li key={item.id} className="list-none">
<a
href={`#${item.id}`}
className="text-[#5c6975] no-underline font-normal py-2 pr-0 block relative hover:text-blue-500"
>
{item.title}
</a>
</li>
);
});
}

return (
<aside className="sidebar right">
<ul data-cy="outline" className="rightSideBarUL list-revert pl-8">
{navItems?.length > 0 && (
<>
<li className="rightSideTitle">CONTENTS</li>
{navItems}
</>
)}
</ul>
</aside>
);
};

export default PrismicSidebarLayout;
20 changes: 17 additions & 3 deletions site/gatsby-site/src/components/blog/PrismicBlogPost.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@ import React, { useEffect } from 'react';
import { Trans } from 'react-i18next';
import config from '../../../config';
import { useLayoutContext } from 'contexts/LayoutContext';
import Outline from 'components/Outline';
import PrismicOutline from 'components/PrismicOutline';
import { extractHeaders } from 'utils/extractHeaders';
import { Heading1, Heading2 } from 'components/CustomHeaders';

const PrismicBlogPost = ({ post, location }) => {
let headers = [];

const extractedHeaders = extractHeaders(post.data.content);

headers = extractedHeaders;

// Define custom components for Prismic Rich Text
const components = {
heading1: ({ children }) => <Heading1>{children}</Heading1>,
heading2: ({ children }) => <Heading2>{children}</Heading2>,
};

const metaTitle = post.data.metatitle;

const canonicalUrl = config.gatsby.siteUrl + location.pathname;
Expand All @@ -19,7 +33,7 @@ const PrismicBlogPost = ({ post, location }) => {

const rightSidebar = (
<>
<Outline location={loc} />
<PrismicOutline location={loc} tableOfContents={headers} />
</>
);

Expand Down Expand Up @@ -61,7 +75,7 @@ const PrismicBlogPost = ({ post, location }) => {
</span>
</div>
<div className="prose">
<PrismicRichText field={post.data.content.richText} />
<PrismicRichText field={post.data.content.richText} components={components} />
</div>
</>
);
Expand Down
55 changes: 26 additions & 29 deletions site/gatsby-site/src/components/doc/PrismicDocPost.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import React, { useEffect } from 'react';
import { PrismicRichText } from '@prismicio/react';
import TranslationBadge from 'components/i18n/TranslationBadge';
import { Link } from 'gatsby';
import React, { useEffect } from 'react';
import { Trans } from 'react-i18next';
import config from '../../../config';
import { useLayoutContext } from 'contexts/LayoutContext';
import Outline from 'components/Outline';
import PrismicOutline from 'components/PrismicOutline';
import Leaderboards from 'components/landing/Leaderboards';
import Sponsors from 'components/landing/Sponsors';
import { RichText } from 'prismic-reactjs';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import TranslationBadge from 'components/i18n/TranslationBadge';
import { extractHeaders } from 'utils/extractHeaders';
import { Heading1, Heading2 } from 'components/CustomHeaders';

const PrismicDocPost = ({ doc, location }) => {
let headers = [];

const extractedHeaders = extractHeaders(doc.data.content);

headers = extractedHeaders;

// Define custom components for Prismic Rich Text
const components = {
heading1: ({ children }) => <Heading1>{children}</Heading1>,
heading2: ({ children }) => <Heading2>{children}</Heading2>,
Leaderboards: <Leaderboards />,
Sponsors: <Sponsors />,
};
Expand All @@ -24,7 +32,7 @@ const PrismicDocPost = ({ doc, location }) => {

const rightSidebar = (
<>
<Outline location={loc} />
<PrismicOutline location={loc} tableOfContents={headers} />
</>
);

Expand All @@ -36,7 +44,7 @@ const PrismicDocPost = ({ doc, location }) => {

return (
<>
<div className={'titleWrapper'}>
<div className="titleWrapper">
<h1>{doc.data.title}</h1>
</div>
<div className="flex items-center mb-6 -mt-1 flex-wrap">
Expand All @@ -49,27 +57,16 @@ const PrismicDocPost = ({ doc, location }) => {
</>
)}
</div>
{doc.data.content.map((content, index) => {
return (
<>
{content.markdown && (
<div className="prose">
{(() => {
const rawMarkdown = RichText.asText(content.markdown.richText);

return <ReactMarkdown remarkPlugins={[remarkGfm]}>{rawMarkdown}</ReactMarkdown>;
})()}
</div>
)}
{content.text && (
<div className="prose">
<PrismicRichText key={index} field={content.text.richText} />
</div>
)}
{content.component && <div className=" mt-4">{components[content.component]}</div>}
</>
);
})}
{doc.data.content.map((content, index) => (
<>
{content.text && (
<div className="prose">
<PrismicRichText key={index} field={content.text.richText} components={components} />
</div>
)}
{content.component && <div className="mt-4">{components[content.component]}</div>}
</>
))}
</>
);
};
Expand Down
36 changes: 36 additions & 0 deletions site/gatsby-site/src/utils/extractHeaders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import slugify from 'slugify';

// Extract headers from a content object
export const extractHeaders = (content) => {
const headers = [];

let richText;

if (content.length > 0) {
content.forEach((block) => {
richText = block.text.richText;
headers.push(...extractHeadersFromRichText(richText));
});
} else {
richText = content.richText;
headers.push(...extractHeadersFromRichText(richText));
}

return headers;
};

// Extract headers from a rich text object and return an array of headers with id and title
const extractHeadersFromRichText = (richText) => {
const headers = [];

richText.forEach((block) => {
if (block.type === 'heading1' || block.type === 'heading2') {
headers.push({
id: slugify(block.text, { lower: true }),
title: block.text,
});
}
});

return headers;
};

0 comments on commit df12e56

Please sign in to comment.