Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add table of contents in case study #1673

Merged
merged 32 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
cc433c0
feat: refactor case study code
Shurtu-gal May 15, 2023
2973f5e
feat: add table of contents
Shurtu-gal May 15, 2023
31254ac
fix: remove case-study-layout
Shurtu-gal May 16, 2023
8977a3a
fix: selected item not getting bold
Shurtu-gal May 17, 2023
3653f4f
Merge branch 'master' into feat/table-of-contents
derberg May 22, 2023
1152cad
fix: responsiveness
Shurtu-gal May 23, 2023
1531f03
Merge remote-tracking branch 'upstream/master' into feat/table-of-con…
Shurtu-gal May 23, 2023
525994d
fix: replace map with join for arrays
Shurtu-gal May 23, 2023
c66bd68
Merge branch 'master' into feat/table-of-contents
Shurtu-gal May 24, 2023
c4621f2
Merge branch 'master' into feat/table-of-contents
Shurtu-gal May 27, 2023
92b18ee
fix: max-width issues
Shurtu-gal May 28, 2023
1e32ce5
fix: max-width issues
Shurtu-gal May 28, 2023
2517330
chore: fix width issue across browsers
Shurtu-gal Jul 21, 2023
a0f72fb
Merge branch 'master' into feat/table-of-contents
akshatnema Sep 30, 2023
61fd90e
chore: commit [suggestion](https://github.com/asyncapi/website/pull/1…
Shurtu-gal Oct 1, 2023
ebe5629
chore(refactor): move array to different file
Shurtu-gal Oct 1, 2023
e1dc71c
fix: move it to staticHelpers
Shurtu-gal Oct 1, 2023
894268a
Merge branch 'master' into feat/table-of-contents
Shurtu-gal Oct 1, 2023
6563388
Merge remote-tracking branch 'upstream/master' into feat/table-of-con…
Shurtu-gal Nov 8, 2023
a2d7dba
feat: add collapsing and custom hook
Shurtu-gal Nov 8, 2023
bb9f784
chore: remove redundant scrollspy and style drop down button
Shurtu-gal Nov 9, 2023
c63ffa7
Merge branch 'master' into feat/table-of-contents
Shurtu-gal Nov 9, 2023
13cf0f8
feat: keep parent active when child is active
Shurtu-gal Nov 9, 2023
dbe4d33
Merge branch 'master' into feat/table-of-contents
akshatnema Dec 7, 2023
03cbadc
Merge branch 'master' into feat/table-of-contents
akshatnema Dec 7, 2023
c29457f
Merge branch 'master' into feat/table-of-contents
Shurtu-gal Dec 8, 2023
a292ac3
Merge branch 'master' into feat/table-of-contents
Shurtu-gal Dec 12, 2023
d14dafa
Merge branch 'master' into feat/table-of-contents
akshatnema Dec 16, 2023
de0b32c
Merge branch 'master' into feat/table-of-contents
derberg Dec 19, 2023
cd6ea4a
Merge branch 'master' into feat/table-of-contents
akshatnema Dec 20, 2023
4f80d1e
Merge branch 'master' into feat/table-of-contents
akshatnema Dec 20, 2023
7d3565c
Merge branch 'master' into feat/table-of-contents
Shurtu-gal Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions components/CaseTOC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useMemo, useState } from "react";
import Scrollspy from "react-scrollspy";
import { twMerge } from "tailwind-merge";
import ArrowRight from "./icons/ArrowRight";
import { useHeadingsObserver } from "./helpers/useHeadingsObserver";

const checkIfActive = (item, currSelected) => {
return item.slug === currSelected || item.children?.some((child) => checkIfActive(child, currSelected));
}

const convertContentToTocItems = (content, level = 1) => {
const tocItems = [];

for (let section of content) {
const item = {
lvl: level,
content: section.title,
slug: section.title
.replace(/<|>|"|\\|\/|=/gi, "")
.replace(/\s/gi, "-")
.toLowerCase(),
};

if (section.children && section.children.length > 0) {
const children = convertContentToTocItems(section.children, level + 1);
item.children = children;
}

tocItems.push(item);
}

return tocItems;
};

function TOCItem({ item, index, currSelected, closeMenu }) {
const [open, setOpen] = useState(false);
const handleClick = () => {
closeMenu();
setOpen(false);
};
const active = useMemo(() => checkIfActive(item, currSelected), [item, currSelected]);

return (
<>
<nav className="relative block max-w-max">
<a
className={`mb-1 transition duration-100 ease-in-out text-gray-900 font-normal text-sm font-sans antialiased hover:underline flex items-center ${
active && "text-primary-500 font-bold"
}`}
href={`#${item.slug}`}
key={index}
style={{ marginLeft: `${(item.lvl - 1) * 16}px` }}
onClick={handleClick}
>
{item.content}
</a>
{item.children && item.children.length > 0 && (
<span onClick={() => setOpen(!open)} className="cursor-pointer absolute -right-6 top-0 ">
<ArrowRight
className={`${
open ? "rotate-90" : "0"
} transform transition duration-200 ease-in-out h-5 text-gray-500`}
/>
</span>
)}
</nav>
{item.children && item.children.length > 0 && (
<ul
className={`left-0 relative ${
open ? "max-h-[1000px]" : "max-h-[0.01px]"
} overflow-hidden transition-all duration-300 ease-in-out`}
>
{item.children.map((item, index) => (
<TOCItem
item={item}
index={index}
key={index}
closeMenu={closeMenu}
currSelected={currSelected}
/>
))}
</ul>
)}
</>
);
}

export default function CaseTOC({
className,
cssBreakingPoint = "xl",
toc,
contentSelector,
}) {
if (!toc || !toc.length) return null;
const tocItems = useMemo(() => convertContentToTocItems(toc), [toc]);
const [open, setOpen] = useState(false);
const { currActive: selected } = useHeadingsObserver();

return (
<div
className={twMerge(
`${className} ${tocItems.length ? "" : "hidden"} ${
cssBreakingPoint === "xl" ? "xl:block" : "lg:block"
} md:top-24 md:max-h-(screen-14) z-20`,
)}
>
<div
className={`flex cursor-pointer ${tocItems.length ? "" : "hidden"} ${
cssBreakingPoint === "xl" ? "xl:cursor-auto" : "lg:cursor-auto"
} xl:mt-2`}
>
<h5
className={twMerge(
`${
open && "mb-4"
} flex-1 text-primary-500 font-medium uppercase tracking-wide text-sm font-sans antialiased ${
cssBreakingPoint === "xl"
? "xl:mb-4 xl:text-xs xl:text-gray-900 xl:font-bold"
: "lg:mb-4 lg:text-xs lg:text-gray-900 lg:font-bold"
}`,
)}
>
On this page
</h5>
<div
className={`text-underline text-center p4 ${
cssBreakingPoint === "xl" ? "xl:hidden" : "lg:hidden"
}`}
onClick={() => setOpen(!open)}
>
<ArrowRight
className={`${
open ? "-rotate-90" : "rotate-90"
} transform transition duration-200 ease-in-out h-6 -mt-0.5 text-primary-500`}
/>
</div>
</div>
<div
className={`${!open && "hidden"} ${
cssBreakingPoint === "xl" ? "xl:block" : "lg:block"
}`}
>
<ul className="mt-2">
{tocItems.map((item, index) => (
<TOCItem
item={item}
index={index}
key={index}
closeMenu={() => setOpen(false)}
currSelected={selected}
/>
))}
</ul>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion components/TOC.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function TOC({
</div>
<div className={`${!open && 'hidden'} ${cssBreakingPoint === 'xl' ? 'xl:block' : 'lg:block'}`}>
<Scrollspy
items={tocItems.map(item => item.slugWithATag)}
items={tocItems.map(item => item.slug)}
currentClassName="text-primary-500 font-bold"
componentTag="div"
rootEl={contentSelector}
Expand Down
36 changes: 36 additions & 0 deletions components/helpers/useHeadingsObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useRef, useState } from "react";

/**
* @description Custom hook to observe headings and set the current active heading
* @example const { currActive } = useHeadingsObserver();
* @returns {object} currActive - current active heading
*/
export function useHeadingsObserver() {
const observer = useRef(null);
const headingsRef = useRef([]);
const [currActive, setCurrActive] = useState(null);

useEffect(() => {
const callback = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setCurrActive(entry.target.id);
}
})
}

// The heading in from top 20% of the viewport to top 30% of the viewport will be considered as active
observer.current = new IntersectionObserver(callback, {
rootMargin: '-20% 0px -70% 0px',
});

headingsRef.current = document.querySelectorAll('h2, h3');
headingsRef.current.forEach(heading => {
observer.current.observe(heading);
})

return () => observer.current.disconnect();
}, []);

return { currActive }
}
5 changes: 3 additions & 2 deletions components/typography/Heading.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ export default function Heading({
level = 'h2',
textColor = 'text-primary-800',
className,
children
children,
id
}) {
let classNames = ''
const Tag = `${level}`;
Expand Down Expand Up @@ -46,7 +47,7 @@ export default function Heading({
}

return (
<Tag className={`${textColor} ${classNames}`}>
<Tag className={`${textColor} ${classNames}`} id={id}>
Shurtu-gal marked this conversation as resolved.
Show resolved Hide resolved
{children}
</Tag>
)
Expand Down
106 changes: 106 additions & 0 deletions lib/staticHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,109 @@ export function getEvents(events, size) {

return meetingsWithDates;
}

export const generateCaseStudyContent = (data) => {
const { challenges, solution, usecase, architecture, testing, codegen, schemaStorage, registry, versioning, validation, asyncapiStorage, asyncapiEditing, asyncapiExtensions, asyncapiDocumentation, asyncapiBindings, asyncapiTools, additionalResources, casestudy } = data;
const languages= casestudy.technical.languages
const frameworks=casestudy.technical.frameworks
const protocols=casestudy.technical.protocols
const versions=casestudy.asyncapi.versions

return [
{
title: "Challenges",
content: challenges,
},
{
title: "Solution",
content: solution,
},
{
title: "Use Case",
content: usecase,
},
{
title: "More Details",
items: [
`Languages: ${languages.join(", ")}`,
`Frameworks: ${frameworks.join(", ")}`,
`Protocols: ${protocols.join(", ")}`,
],
children: [
{
title: "Testing strategy",
content: testing,
},
{
title: "Approach to code generation",
content: codegen,
},
{
title: "Architecture",
content: architecture,
},
{
title: "More Details about AsyncAPI",
items: [
`Version: ${versions.join(", ")}`,
`Who maintains documents: ${casestudy.asyncapi.maintainers}}`,
`Internal users: ${casestudy.asyncapi.audience.internal.toString()}`,
`External users: ${casestudy.asyncapi.audience.external.toString()}`,
],
children: [
{
title: "How AsyncAPI documents are stored",
content: asyncapiStorage,
},
{
title: "Where maintainers edit AsyncAPI documents",
content: asyncapiEditing,
},
{
title: "What extensions are used",
content: asyncapiExtensions,
},
{
title: "How documentation is generated",
content: asyncapiDocumentation,
},
{
title: "What bindings are used",
content: asyncapiBindings,
},
{
title: "What tools are used",
content: asyncapiTools,
},
],
},
{
title: "Schemas",
items: [`Spec: ${casestudy.schemas.description}`],
children: [
{
title: "Storage strategy",
content: schemaStorage,
},
{
title: "Schema Registry",
content: registry,
},
{
title: "Versioning of schemas",
content: versioning,
},
{
title: "Validation of message schemas",
content: validation,
},
{
title: "Additional Resources",
content: additionalResources,
},
],
},
],
},
];
}
Loading