Migrated Developer Docs (#5683)

- Migrated developer docs to Twenty website

- Modified User Guide and Docs layout to include sections and
subsections

**Section Example:**
<img width="549" alt="Screenshot 2024-05-30 at 15 44 42"
src="https://github.com/twentyhq/twenty/assets/102751374/41bd4037-4b76-48e6-bc79-48d3d6be9ab8">

**Subsection Example:**
<img width="557" alt="Screenshot 2024-05-30 at 15 44 55"
src="https://github.com/twentyhq/twenty/assets/102751374/f14c65a9-ab0c-4530-b624-5b20fc00511a">


- Created different components (Tabs, Tables, Editors etc.) for the mdx
files

**Tabs & Editor**

<img width="665" alt="Screenshot 2024-05-30 at 15 47 39"
src="https://github.com/twentyhq/twenty/assets/102751374/5166b5c7-b6cf-417d-9f29-b1f674c1c531">

**Tables**

<img width="698" alt="Screenshot 2024-05-30 at 15 57 39"
src="https://github.com/twentyhq/twenty/assets/102751374/2bbfe937-ec19-4004-ab00-f7a56e96db4a">

<img width="661" alt="Screenshot 2024-05-30 at 16 03 32"
src="https://github.com/twentyhq/twenty/assets/102751374/ae95b47c-dd92-44f9-b535-ccdc953f71ff">

- Created a crawler for Twenty Developers (now that it will be on the
twenty website). Once this PR is merged and the website is re-deployed,
we need to start crawling and make sure the index name is
‘twenty-developer’
- Added a dropdown menu in the header to access User Guide and
Developers + added Developers to footer


https://github.com/twentyhq/twenty/assets/102751374/1bd1fbbd-1e65-4461-b18b-84d4ddbb8ea1

- Made new layout responsive

Please fill in the information for each mdx file so that it can appear
on its card, as well as in the ‘In this article’ section. Example with
‘Getting Started’ in the User Guide:

<img width="786" alt="Screenshot 2024-05-30 at 16 29 39"
src="https://github.com/twentyhq/twenty/assets/102751374/2714b01d-a664-4ddc-9291-528632ee12ea">

Example with info and sectionInfo filled in for 'Getting Started':

<img width="620" alt="Screenshot 2024-05-30 at 16 33 57"
src="https://github.com/twentyhq/twenty/assets/102751374/bc69e880-da6a-4b7e-bace-1effea866c11">


Please keep in mind that the images that are being used for Developers
are the same as those found in User Guide and may not match the article.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Ady Beraud
2024-06-03 19:52:43 +03:00
committed by GitHub
parent f7cdd14c75
commit 671de4170f
139 changed files with 7057 additions and 494 deletions

View File

@ -0,0 +1,55 @@
import { DocSearch } from '@docsearch/react';
import { StoredDocSearchHit } from '@docsearch/react/dist/esm/types';
interface AlgoliaHit extends StoredDocSearchHit {
_snippetResult?: {
content: { value: string };
};
}
interface AlgoliaDocSearchProps {
pathname: string;
}
export const AlgoliaDocSearch = ({ pathname }: AlgoliaDocSearchProps) => {
const indexName = pathname.includes('user-guide')
? 'user-guide'
: 'developer';
return (
<DocSearch
hitComponent={({ hit }: { hit: AlgoliaHit }) => (
<section className="DocSearch-Hits">
<a href={hit.url}>
<div className="DocSearch-Hit-Container">
<div className="DocSearch-Hit-icon">
<svg width="20" height="20" viewBox="0 0 20 20">
<path
d="M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z"
stroke="currentColor"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
</div>
<div className="DocSearch-Hit-action">
<h2>
{hit.hierarchy.lvl1 ? hit.hierarchy.lvl1 : hit.hierarchy.lvl0}
</h2>
<p
dangerouslySetInnerHTML={{
__html: hit?._snippetResult?.content?.value || '',
}}
></p>
</div>
</div>
</a>
</section>
)}
appId={process.env.NEXT_PUBLIC_ALGOLIA_APP_ID as string}
apiKey={process.env.NEXT_PUBLIC_ALGOLIA_API_KEY as string}
indexName={`twenty-${indexName}`}
/>
);
};

View File

@ -0,0 +1,74 @@
'use client';
import styled from '@emotion/styled';
import { usePathname, useRouter } from 'next/navigation';
import { Theme } from '@/app/_components/ui/theme/theme';
import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
import { getCardPath } from '@/shared-utils/getCardPath';
const StyledContainer = styled.div`
color: ${Theme.border.color.plain};
border: 2px solid ${Theme.border.color.plain};
border-radius: ${Theme.border.radius.md};
gap: ${Theme.spacing(4)};
display: flex;
flex-direction: column;
cursor: pointer;
&:hover {
box-shadow: -8px 8px 0px -4px ${Theme.color.gray60};
}
`;
const StyledHeading = styled.div`
font-size: ${Theme.font.size.lg};
color: ${Theme.text.color.primary};
padding: 0 16px;
font-weight: ${Theme.font.weight.medium};
@media (max-width: 800px) {
font-size: ${Theme.font.size.base};
}
`;
const StyledSubHeading = styled.div`
font-size: ${Theme.font.size.xs};
color: ${Theme.text.color.secondary};
font-family: ${Theme.font.family};
margin: 0 16px 24px;
font-weight: ${Theme.font.weight.regular};
line-height: 21px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
`;
const StyledImage = styled.img`
border-bottom: 1.5px solid #14141429;
height: 160px;
border-top-right-radius: 6px;
border-top-left-radius: 6px;
`;
export default function DocsCard({
card,
isSection = false,
}: {
card: DocsArticlesProps;
isSection?: boolean;
}) {
const router = useRouter();
const pathname = usePathname();
const path = getCardPath(card, pathname, isSection);
if (card.title) {
return (
<StyledContainer onClick={() => router.push(path)}>
<StyledImage src={card.image} alt={card.title} />
<StyledHeading>{card.title}</StyledHeading>
<StyledSubHeading>{card.info}</StyledSubHeading>
</StyledContainer>
);
}
}

View File

@ -0,0 +1,162 @@
'use client';
import React from 'react';
import styled from '@emotion/styled';
import { usePathname } from 'next/navigation';
import { ArticleContent } from '@/app/_components/ui/layout/articles/ArticleContent';
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import { FileContent } from '@/app/_server-utils/get-posts';
import { getUriAndLabel } from '@/shared-utils/pathUtils';
const StyledContainer = styled('div')`
${mq({
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
borderBottom: `1px solid ${Theme.background.transparent.medium}`,
fontFamily: `${Theme.font.family}`,
})};
width: 100%;
min-height: calc(100vh - 50px);
@media (min-width: 990px) {
justify-content: flex-start;
}
`;
const StyledWrapper = styled.div`
@media (max-width: 450px) {
padding: ${Theme.spacing(10)} 30px ${Theme.spacing(20)};
width: 340px;
}
@media (min-width: 451px) and (max-width: 800px) {
padding: ${Theme.spacing(10)} 50px ${Theme.spacing(20)};
width: 440px;
}
@media (min-width: 801px) {
max-width: 720px;
margin: ${Theme.spacing(10)} 92px ${Theme.spacing(20)};
}
@media (min-width: 1500px) {
max-width: 720px;
margin: ${Theme.spacing(10)} auto ${Theme.spacing(20)};
}
`;
const StyledHeader = styled.div`
display: flex;
flex-direction: column;
gap: ${Theme.spacing(8)};
@media (min-width: 450px) and (max-width: 800px) {
width: 340px;
}
`;
const StyledHeading = styled.h1`
font-size: 40px;
font-weight: 700;
font-family: var(--font-gabarito);
margin: 0px;
@media (max-width: 800px) {
font-size: 28px;
}
`;
const StyledHeaderInfoSection = styled.div`
display: flex;
flex-direction: column;
gap: ${Theme.spacing(1)};
`;
const StyledHeaderInfoSectionTitle = styled.div`
font-size: ${Theme.font.size.sm};
padding: ${Theme.spacing(2)} 0px;
color: ${Theme.text.color.secondary};
font-weight: ${Theme.font.weight.medium};
font-family: var(--font-gabarito);
`;
const StyledHeaderInfoSectionSub = styled.p`
display: flex;
flex-direction: column;
gap: ${Theme.spacing(4)};
color: ${Theme.text.color.tertiary};
line-height: 1.8;
margin: 0px;
`;
const StyledRectangle = styled.div`
width: 100%;
height: 1px;
background: ${Theme.background.transparent.medium};
`;
const StyledImageContainer = styled.div`
border: 2px solid ${Theme.text.color.primary};
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: 16px;
max-width: fit-content;
img {
height: 100%;
max-width: 100%;
width: 100%;
@media (min-width: 1000px) {
width: 720px;
}
}
`;
export default function DocsContent({ item }: { item: FileContent }) {
const pathname = usePathname();
const { uri, label } = getUriAndLabel(pathname);
const BREADCRUMB_ITEMS = [
{
uri: uri,
label: label,
},
];
return (
<StyledContainer>
<StyledWrapper>
<StyledHeader>
<Breadcrumbs
items={BREADCRUMB_ITEMS}
activePage={item.itemInfo.title}
separator="/"
/>
<StyledHeading>{item.itemInfo.title}</StyledHeading>
<StyledImageContainer>
{item.itemInfo.image && (
<img
id={`img-${item.itemInfo.title}`}
src={item.itemInfo.image}
alt={item.itemInfo.title}
/>
)}
</StyledImageContainer>
<StyledHeaderInfoSection>
<StyledHeaderInfoSectionTitle>
In this article
</StyledHeaderInfoSectionTitle>
<StyledHeaderInfoSectionSub>
{item.itemInfo.info}
</StyledHeaderInfoSectionSub>
</StyledHeaderInfoSection>
<StyledRectangle />
</StyledHeader>
<ArticleContent>{item.content}</ArticleContent>
</StyledWrapper>
</StyledContainer>
);
}

View File

@ -0,0 +1,187 @@
'use client';
import styled from '@emotion/styled';
import { usePathname } from 'next/navigation';
import DocsCard from '@/app/_components/docs/DocsCard';
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
import { constructSections } from '@/shared-utils/constructSections';
import { filterDocsIndex } from '@/shared-utils/filterDocsIndex';
import { getUriAndLabel } from '@/shared-utils/pathUtils';
const StyledContainer = styled.div`
${mq({
width: ['100%', '60%', '60%'],
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
})};
@media (min-width: 1500px) {
width: 100%;
}
`;
const StyledWrapper = styled.div`
padding: ${Theme.spacing(10)} 92px ${Theme.spacing(20)};
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 450px) {
padding: ${Theme.spacing(10)} 30px ${Theme.spacing(20)};
align-items: flex-start;
width: 340px;
}
@media (min-width: 450px) and (max-width: 800px) {
padding: ${Theme.spacing(10)} 50px ${Theme.spacing(20)};
align-items: flex-start;
width: 440px;
}
@media (min-width: 1500px) {
width: 720px;
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)};
margin-right: 300px;
}
`;
const StyledTitle = styled.div`
font-size: ${Theme.font.size.sm};
color: ${Theme.text.color.quarternary};
font-weight: ${Theme.font.weight.medium};
width: 100%;
@media (min-width: 450px) and (max-width: 800px) {
width: 340px;
display: flex;
align-items: center;
}
`;
const StyledSection = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
@media (min-width: 801px) {
align-items: flex-start;
}
&:not(:last-child) {
margin-bottom: 50px;
}
`;
const StyledHeader = styled.div`
display: flex;
flex-direction: column;
gap: 0px;
width: 100%;
@media (min-width: 450px) and (max-width: 1200px) {
width: 340px;
margin-bottom: 24px;
}
@media (min-width: 450px) and (max-width: 800px) {
margin-bottom: 24px;
width: 340px;
}
`;
const StyledHeading = styled.h1`
line-height: 38px;
font-weight: 700;
font-size: 40px;
color: ${Theme.text.color.primary};
margin: 0px;
margin-top: 32px;
@media (max-width: 800px) {
font-size: 28px;
}
`;
const StyledSubHeading = styled.h1`
line-height: 28.8px;
font-size: ${Theme.font.size.lg};
font-weight: ${Theme.font.weight.regular};
color: ${Theme.text.color.tertiary};
@media (max-width: 800px) {
font-size: ${Theme.font.size.sm};
}
`;
const StyledContent = styled.div`
${mq({
width: '100%',
paddingTop: `${Theme.spacing(6)}`,
display: ['flex', 'flex', 'grid'],
flexDirection: 'column',
gridTemplateRows: 'auto auto',
gridTemplateColumns: 'auto auto',
gap: `${Theme.spacing(6)}`,
})};
@media (min-width: 450px) {
justify-content: flex-start;
width: 340px;
}
`;
interface DocsProps {
docsArticleCards: DocsArticlesProps[];
isSection?: boolean;
}
export default function DocsMain({
docsArticleCards,
isSection = false,
}: DocsProps) {
const sections = constructSections(docsArticleCards, isSection);
const pathname = usePathname();
const { uri, label } = getUriAndLabel(pathname);
const BREADCRUMB_ITEMS = [
{
uri: uri,
label: label,
},
];
return (
<StyledContainer>
<StyledWrapper>
{isSection ? (
<Breadcrumbs
items={BREADCRUMB_ITEMS}
activePage={sections[0].name}
separator="/"
/>
) : (
<StyledTitle>{label}</StyledTitle>
)}
{sections.map((section, index) => {
const filteredArticles = isSection
? docsArticleCards
: filterDocsIndex(docsArticleCards, section.name);
return (
<StyledSection key={index}>
<StyledHeader>
<StyledHeading>{section.name}</StyledHeading>
<StyledSubHeading>{section.info}</StyledSubHeading>
</StyledHeader>
<StyledContent>
{filteredArticles.map((card) => (
<DocsCard
key={card.title}
card={card}
isSection={isSection}
/>
))}
</StyledContent>
</StyledSection>
);
})}
</StyledWrapper>
</StyledContainer>
);
}

View File

@ -0,0 +1,51 @@
'use client';
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { usePathname } from 'next/navigation';
import DocsSidebar from '@/app/_components/docs/DocsSideBar';
import DocsTableContents from '@/app/_components/docs/TableContent';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
import {
isPlaygroundPage,
shouldShowEmptySidebar,
} from '@/shared-utils/pathUtils';
const StyledContainer = styled.div`
width: 100%;
display: flex;
flex-direction: row;
border-bottom: 1px solid ${Theme.background.transparent.medium};
min-height: calc(100vh - 50px);
`;
const StyledEmptySideBar = styled.div`
${mq({
width: '20%',
display: ['none', 'none', ''],
})};
`;
export const DocsMainLayout = ({
children,
docsIndex,
}: {
children: ReactNode;
docsIndex: DocsArticlesProps[];
}) => {
const pathname = usePathname();
return (
<StyledContainer>
{!isPlaygroundPage(pathname) && <DocsSidebar docsIndex={docsIndex} />}
{children}
{shouldShowEmptySidebar(pathname) ? (
<StyledEmptySideBar />
) : (
<DocsTableContents />
)}
</StyledContainer>
);
};

View File

@ -0,0 +1,118 @@
'use client';
import styled from '@emotion/styled';
import { usePathname, useRouter } from 'next/navigation';
import { AlgoliaDocSearch } from '@/app/_components/docs/AlgoliaDocSearch';
import DocsSidebarSection from '@/app/_components/docs/DocsSidebarSection';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
import { getSectionIcon } from '@/shared-utils/getSectionIcons';
import '@docsearch/css';
import '../../user-guide/algolia.css';
const StyledContainer = styled.div`
${mq({
display: ['none', 'flex', 'flex'],
flexDirection: 'column',
background: `${Theme.background.secondary}`,
borderRight: `1px solid ${Theme.background.transparent.medium}`,
borderBottom: `1px solid ${Theme.background.transparent.medium}`,
padding: `${Theme.spacing(10)} ${Theme.spacing(4)}`,
gap: `${Theme.spacing(6)}`,
})}
width: 300px;
min-width: 300px;
overflow: scroll;
height: calc(100vh - 60px);
position: sticky;
top: 64px;
`;
const StyledHeading = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: ${Theme.spacing(2)};
font-size: ${Theme.font.size.sm};
font-weight: ${Theme.font.weight.medium};
margin-bottom: 8px;
`;
const StyledIconContainer = styled.div`
width: 24px;
height: 24px;
display: flex;
flex-direction: row;
justify-content: center;
font-size: ${Theme.font.size.sm};
font-weight: ${Theme.font.weight.medium};
color: ${Theme.text.color.secondary};
border: 1px solid ${Theme.text.color.secondary};
border-radius: ${Theme.border.radius.sm};
padding: ${Theme.spacing(1)};
`;
const StyledHeadingText = styled.h1`
cursor: pointer;
font-size: ${Theme.font.size.sm};
font-weight: ${Theme.font.weight.medium};
color: ${Theme.text.color.secondary};
`;
const DocsSidebar = ({ docsIndex }: { docsIndex: DocsArticlesProps[] }) => {
const router = useRouter();
const pathName = usePathname();
const path = pathName.includes('user-guide')
? '/user-guide'
: pathName.includes('developers')
? '/developers'
: '/twenty-ui';
const sections = Array.from(
new Set(docsIndex.map((guide) => guide.section)),
).map((section) => ({
name: section,
icon: getSectionIcon(section),
guides: docsIndex.filter((guide) => {
const isInSection = guide.section === section;
const hasFiles = guide.numberOfFiles > 0;
const isNotSingleFileTopic = !(
guide.numberOfFiles > 1 && guide.topic === guide.title
);
return isInSection && hasFiles && isNotSingleFileTopic;
}),
}));
return (
<StyledContainer>
<AlgoliaDocSearch pathname={pathName} />
{sections.map((section) => (
<div key={section.name}>
<StyledHeading>
<StyledIconContainer>{section.icon}</StyledIconContainer>
<StyledHeadingText
onClick={() =>
router.push(
section.name === 'User Guide'
? '/user-guide'
: section.name === 'Developers'
? '/developers'
: path,
)
}
>
{section.name}
</StyledHeadingText>
</StyledHeading>
<DocsSidebarSection docsIndex={section.guides} />
</div>
))}
</StyledContainer>
);
};
export default DocsSidebar;

View File

@ -0,0 +1,214 @@
'use client';
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { IconPoint } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import { IconChevronDown, IconChevronRight } from '@/app/_components/ui/icons';
import { Theme } from '@/app/_components/ui/theme/theme';
import { DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
import { groupArticlesByTopic } from '@/content/user-guide/constants/groupArticlesByTopic';
import { getCardPath } from '@/shared-utils/getCardPath';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledIndex = styled.div`
margin-bottom: 8px;
`;
const StyledTitle = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
gap: ${Theme.spacing(2)};
color: ${Theme.text.color.quarternary};
margin-top: 8px;
padding-bottom: ${Theme.spacing(2)};
font-family: ${Theme.font.family};
font-size: ${Theme.font.size.xs};
font-weight: 600;
`;
const StyledSubTopicItem = styled.a<{ isselected: boolean }>`
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
height: ${Theme.spacing(8)};
color: ${(props) =>
props.isselected ? Theme.text.color.primary : Theme.text.color.secondary};
font-weight: ${(props) =>
props.isselected ? Theme.font.weight.medium : Theme.font.weight.regular};
font-family: ${Theme.font.family};
font-size: ${Theme.font.size.xs};
gap: 19px;
padding: ${(props) =>
props.isselected ? '6px 12px 6px 11px' : '0px 12px 0px 11px'};
background: ${(props) =>
props.isselected
? Theme.background.transparent.light
: Theme.background.secondary};
border-radius: ${Theme.border.radius.md};
text-decoration: none;
&:focus,
&:hover,
&:visited,
&:link,
&:active {
text-decoration: none;
}
&:hover {
background: #1414140a;
}
&:active {
background: #1414140f;
}
`;
const StyledIcon = styled.div`
padding: 0px 4px 0px 4px;
display: flex;
align-items: center;
`;
const StyledIconContainer = styled.div`
margin-top: 3px;
margin-left: -8px;
color: ${Theme.color.gray30};
`;
const StyledCardTitle = styled.p`
margin: 0px -5px;
color: ${Theme.color.gray30};
font-weight: 600;
`;
const StyledRectangle = styled.div<{ isselected: boolean; isHovered: boolean }>`
height: ${(props) =>
props.isselected ? '95%' : props.isHovered ? '70%' : '100%'};
width: 2px;
background: ${(props) =>
props.isselected
? Theme.border.color.plain
: props.isHovered
? Theme.background.transparent.strong
: Theme.background.transparent.light};
transition: height 0.2s ease-in-out;
`;
interface TopicsState {
[topic: string]: boolean;
}
const DocsSidebarSection = ({
docsIndex,
}: {
docsIndex: DocsArticlesProps[];
}) => {
const pathname = usePathname();
const router = useRouter();
const topics = groupArticlesByTopic(docsIndex);
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const path = pathname.includes('user-guide')
? '/user-guide/'
: pathname.includes('developers')
? '/developers/'
: '/twenty-ui';
const initializeUnfoldedState = () => {
const unfoldedState: TopicsState = {};
Object.keys(topics).forEach((topic) => {
const containsCurrentArticle = topics[topic].some((card) => {
const topicPath = card.topic.toLowerCase().replace(/\s+/g, '-');
return pathname.includes(topicPath);
});
unfoldedState[topic] = containsCurrentArticle;
});
return unfoldedState;
};
const [unfolded, setUnfolded] = useState<TopicsState>(
initializeUnfoldedState,
);
useEffect(() => {
const newUnfoldedState = initializeUnfoldedState();
setUnfolded(newUnfoldedState);
}, [pathname]);
const toggleFold = (topic: string) => {
setUnfolded((prev: TopicsState) => ({ ...prev, [topic]: !prev[topic] }));
};
return (
<StyledContainer>
{Object.entries(topics).map(([topic, cards]) => {
const hasMultipleFiles = cards.some((card) => card.numberOfFiles > 1);
return (
<StyledIndex key={topic}>
{hasMultipleFiles ? (
<StyledTitle onClick={() => toggleFold(topic)}>
{unfolded[topic] ? (
<StyledIcon>
<IconChevronDown size={Theme.icon.size.md} />
</StyledIcon>
) : (
<StyledIcon>
<IconChevronRight size={Theme.icon.size.md} />
</StyledIcon>
)}
<div>{topic}</div>
</StyledTitle>
) : null}
{(unfolded[topic] || !hasMultipleFiles) &&
cards.map((card) => {
const isselected = pathname === `${path}${card.fileName}`;
const sectionName = card.topic
.toLowerCase()
.replace(/\s+/g, '-');
const routerPath = getCardPath(card, path, false, sectionName);
return (
<StyledSubTopicItem
key={card.title}
isselected={isselected}
href={routerPath}
onClick={() => router.push(routerPath)}
onMouseEnter={() => setHoveredItem(card.title)}
onMouseLeave={() => setHoveredItem(null)}
>
{card.numberOfFiles > 1 ? (
<>
<StyledRectangle
isselected={isselected}
isHovered={hoveredItem === card.title}
/>
{card.title}
</>
) : (
<>
<StyledIconContainer>
<IconPoint size={Theme.icon.size.md} />
</StyledIconContainer>
<StyledCardTitle>{card.title}</StyledCardTitle>
</>
)}
</StyledSubTopicItem>
);
})}
</StyledIndex>
);
})}
</StyledContainer>
);
};
export default DocsSidebarSection;

View File

@ -0,0 +1,149 @@
'use client';
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { usePathname } from 'next/navigation';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import { useHeadsObserver } from '@/app/user-guide/hooks/useHeadsObserver';
const StyledContainer = styled.div`
${mq({
display: ['none', 'none', 'flex'],
flexDirection: 'column',
background: `${Theme.background.secondary}`,
borderLeft: `1px solid ${Theme.background.transparent.medium}`,
borderBottom: `1px solid ${Theme.background.transparent.medium}`,
padding: `0px ${Theme.spacing(6)}`,
})};
width: 300px;
min-width: 300px;
`;
const StyledNav = styled.nav`
width: 220px;
min-width: 220px;
align-self: flex-start;
padding: 32px 0px;
position: -webkit-sticky;
position: sticky;
top: 70px;
max-height: calc(100vh - 70px);
overflow: auto;
`;
const StyledUnorderedList = styled.ul`
list-style-type: none;
padding: 0;
`;
const StyledList = styled.li`
margin: 12px 0px;
`;
const StyledLink = styled.a`
text-decoration: none;
font-size: 12px;
font-family: var(--font-inter);
color: ${Theme.text.color.tertiary};
&:hover {
color: ${Theme.text.color.secondary};
}
&:active {
color: ${Theme.text.color.primary};
font-weight: 500 !important;
}
`;
const StyledHeadingText = styled.div`
font-size: ${Theme.font.size.sm};
color: ${Theme.text.color.quarternary};
margin-bottom: 20px;
`;
const getStyledHeading = (level: number) => {
switch (level) {
case 3:
return {
marginLeft: 10,
};
case 4:
return {
marginLeft: 20,
};
case 5:
return {
marginLeft: 30,
};
default:
return undefined;
}
};
interface HeadingType {
id: string;
elem: HTMLElement;
className: string;
text: string;
level: number;
}
const DocsTableContents = () => {
const [headings, setHeadings] = useState<HeadingType[]>([]);
const pathname = usePathname();
const { activeText } = useHeadsObserver(pathname);
useEffect(() => {
const nodes: HTMLElement[] = Array.from(
document.querySelectorAll('h2, h3, h4, h5'),
);
const elements: HeadingType[] = nodes.map(
(elem): HeadingType => ({
id: elem.id,
elem: elem,
className: elem.className,
text: elem.innerText,
level: Number(elem.nodeName.charAt(1)),
}),
);
setHeadings(elements);
}, [pathname]);
return (
<StyledContainer>
<StyledNav>
<StyledHeadingText>Table of Content</StyledHeadingText>
<StyledUnorderedList>
{headings.map((heading) => (
<StyledList
key={heading.text}
style={getStyledHeading(heading.level)}
>
<StyledLink
href={`#${heading.text}`}
onClick={(e) => {
e.preventDefault();
const yOffset = -70;
const y =
heading.elem.getBoundingClientRect().top +
window.scrollY +
yOffset;
window.scrollTo({ top: y, behavior: 'smooth' });
}}
style={{
fontWeight: activeText === heading.text ? 'bold' : 'normal',
}}
>
{heading.text}
</StyledLink>
</StyledList>
))}
</StyledUnorderedList>
</StyledNav>
</StyledContainer>
);
};
export default DocsTableContents;