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:
@ -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}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
162
packages/twenty-website/src/app/_components/docs/DocsContent.tsx
Normal file
162
packages/twenty-website/src/app/_components/docs/DocsContent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
187
packages/twenty-website/src/app/_components/docs/DocsMain.tsx
Normal file
187
packages/twenty-website/src/app/_components/docs/DocsMain.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
118
packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx
Normal file
118
packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user