Modifications user guide (#5207)

- Modified layout and responsive
- Added remaining user guide cards
- Added new table of content:


https://github.com/twentyhq/twenty/assets/102751374/007118e5-60f2-4572-90cf-339c134f23c4

-Added fade-in:


https://github.com/twentyhq/twenty/assets/102751374/0669c06d-3eab-484c-a5b5-3857c68f42b5

---------

Co-authored-by: Ady Beraud <a.beraud96@gmail.com>
This commit is contained in:
Ady Beraud 2024-05-01 09:55:57 +03:00 committed by GitHub
parent df5cb9a904
commit 0bc3b6f179
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 479 additions and 284 deletions

View File

@ -1,6 +1,7 @@
'use client';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
const Title = styled.h2`
font-size: 56px;
@ -23,10 +24,16 @@ const Title = styled.h2`
export const Header = () => {
return (
<>
<Title>
Our amazing <br />
<span style={{ color: '#141414' }}>Contributors</span>
</Title>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<Title>
Our amazing <br />
<span style={{ color: '#141414' }}>Contributors</span>
</Title>
</motion.div>
</>
);
};

View File

@ -1,6 +1,7 @@
'use client';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { Theme } from '@/app/_components/ui/theme/theme';
@ -32,15 +33,20 @@ const Description = styled.h2`
export const Header = () => {
return (
<div>
<Title>
Open-source <br /> <span style={{ color: 'black' }}>friends</span>
</Title>
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<Title>
Open-source <br /> <span style={{ color: 'black' }}>friends</span>
</Title>
</motion.div>
<Description>
We are proud to collaborate with a diverse group of partners to promote
open-source software.
</Description>
</div>
</>
);
};

View File

@ -1,6 +1,7 @@
'use client';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
const StyledTitle = styled.div`
margin: 64px auto 0px;
@ -23,9 +24,15 @@ const StyledSubHeader = styled.h1`
export const Title = () => {
return (
<StyledTitle>
<StyledHeader>Latest</StyledHeader>
<StyledSubHeader>Releases</StyledSubHeader>
</StyledTitle>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<StyledTitle>
<StyledHeader>Latest</StyledHeader>
<StyledSubHeader>Releases</StyledSubHeader>
</StyledTitle>
</motion.div>
);
};

View File

@ -77,7 +77,6 @@ export const FooterDesktop = () => {
<div
style={{
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
display: 'flex',
flexDirection: 'row',
@ -126,7 +125,6 @@ export const FooterDesktop = () => {
<div
style={{
width: '100%',
maxWidth: '1200px',
margin: '0 auto',
display: 'flex',
flexDirection: 'row',

View File

@ -96,8 +96,8 @@ export default function ArticleEditContent({
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M10 0.449951C4.475 0.449951 0 4.92495 0 10.45C0 14.875 2.8625 18.6125 6.8375 19.9375C7.3375 20.025 7.525 19.725 7.525 19.4625C7.525 19.225 7.5125 18.4375 7.5125 17.6C5 18.0625 4.35 16.9875 4.15 16.425C4.0375 16.1375 3.55 15.25 3.125 15.0125C2.775 14.825 2.275 14.3625 3.1125 14.35C3.9 14.3375 4.4625 15.075 4.65 15.375C5.55 16.8875 6.9875 16.4625 7.5625 16.2C7.65 15.55 7.9125 15.1125 8.2 14.8625C5.975 14.6125 3.65 13.75 3.65 9.92495C3.65 8.83745 4.0375 7.93745 4.675 7.23745C4.575 6.98745 4.225 5.96245 4.775 4.58745C4.775 4.58745 5.6125 4.32495 7.525 5.61245C8.325 5.38745 9.175 5.27495 10.025 5.27495C10.875 5.27495 11.725 5.38745 12.525 5.61245C14.4375 4.31245 15.275 4.58745 15.275 4.58745C15.825 5.96245 15.475 6.98745 15.375 7.23745C16.0125 7.93745 16.4 8.82495 16.4 9.92495C16.4 13.7625 14.0625 14.6125 11.8375 14.8625C12.2 15.175 12.5125 15.775 12.5125 16.7125C12.5125 18.05 12.5 19.125 12.5 19.4625C12.5 19.725 12.6875 20.0375 13.1875 19.9375C17.1375 18.6125 20 14.8625 20 10.45C20 4.92495 15.525 0.449951 10 0.449951Z"
fill="white"
/>

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 UserGuideTableContents = () => {
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 UserGuideTableContents;

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { useRouter } from 'next/navigation';
import { Theme } from '@/app/_components/ui/theme/theme';
import { UserGuideHomeCardsType } from '@/content/user-guide/constants/UserGuideHomeCards';
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
const StyledContainer = styled.div`
color: ${Theme.border.color.plain};
@ -13,15 +13,10 @@ const StyledContainer = styled.div`
display: flex;
flex-direction: column;
cursor: pointer;
width: 340px;
&:hover {
box-shadow: -8px 8px 0px -4px ${Theme.color.gray60};
}
@media (max-width: 385px) {
width: 280px;
}
`;
const StyledHeading = styled.div`
@ -38,26 +33,34 @@ const StyledSubHeading = styled.div`
font-size: ${Theme.font.size.xs};
color: ${Theme.text.color.secondary};
font-family: ${Theme.font.family};
padding: 0 16px 24px;
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;
`;
export default function UserGuideCard({
card,
}: {
card: UserGuideHomeCardsType;
card: UserGuideArticlesProps;
}) {
const router = useRouter();
return (
<StyledContainer onClick={() => router.push(`/user-guide/${card.url}`)}>
<StyledContainer
onClick={() => router.push(`/user-guide/${card.fileName}`)}
>
<StyledImage src={card.image} alt={card.title} />
<StyledHeading>{card.title}</StyledHeading>
<StyledSubHeading>{card.subtitle}</StyledSubHeading>
<StyledSubHeading>{card.info}</StyledSubHeading>
</StyledContainer>
);
}

View File

@ -10,24 +10,43 @@ import { FileContent } from '@/app/_server-utils/get-posts';
const StyledContainer = styled('div')`
${mq({
width: ['100%', '70%', '60%'],
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`
width: 79.3%;
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)} 0px;
@media (max-width: 450px) {
padding: ${Theme.spacing(10)} 30px ${Theme.spacing(20)};
}
@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)};
}
`;
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.div`
@ -74,33 +93,14 @@ const StyledImageContainer = styled.div`
align-items: center;
overflow: hidden;
border-radius: 16px;
height: 340px;
max-width: fit-content;
@media (max-width: 414px) {
height: 160px;
}
@media (min-width: 415px) and (max-width: 800px) {
height: 240px;
}
@media (min-width: 1500px) {
height: 450px;
}
img {
height: 340px;
@media (max-width: 414px) {
height: 160px;
}
@media (min-width: 415px) and (max-width: 800px) {
height: 240px;
}
@media (min-width: 1500px) {
height: 450px;
height: 100%;
max-width: 100%;
width: 100%;
@media (min-width: 1000px) {
width: 720px;
}
}
`;

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import UserGuideCard from '@/app/_components/user-guide/UserGuideCard';
import { USER_GUIDE_HOME_CARDS } from '@/content/user-guide/constants/UserGuideHomeCards';
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
const StyledContainer = styled.div`
${mq({
@ -21,11 +21,13 @@ const StyledWrapper = styled.div`
flex-direction: column;
width: 100%;
@media (max-width: 800px) {
padding: ${Theme.spacing(10)} 24px ${Theme.spacing(20)};
@media (max-width: 450px) {
padding: ${Theme.spacing(10)} 30px ${Theme.spacing(20)};
align-items: center;
}
@media (max-width: 800px) {
@media (min-width: 450px) and (max-width: 800px) {
padding: ${Theme.spacing(10)} 50px ${Theme.spacing(20)};
align-items: center;
}
`;
@ -35,24 +37,22 @@ const StyledTitle = styled.div`
color: ${Theme.text.color.quarternary};
font-weight: ${Theme.font.weight.medium};
margin-bottom: 32px;
@media (min-width: 380px) and (max-width: 800px) {
width: 100%;
@media (min-width: 450px) and (max-width: 800px) {
width: 340px;
margin-bottom: 24px;
}
@media (max-width: 380px) {
width: 280px;
}
`;
const StyledHeader = styled.div`
display: flex;
flex-direction: column;
gap: 0px;
@media (min-width: 380px) and (max-width: 800px) {
width: 100%;
@media (min-width: 450px) and (max-width: 1200px) {
width: 340px;
}
@media (max-width: 380px) {
width: 280px;
margin-bottom: 24px;
}
`;
@ -87,15 +87,19 @@ const StyledContent = styled.div`
gridTemplateColumns: 'auto auto',
gap: `${Theme.spacing(6)}`,
})};
@media (max-width: 810px) {
align-items: center;
}
@media (min-width: 1200px) {
justify-content: left;
@media (min-width: 450px) {
justify-content: flex-start;
width: 340px;
}
`;
export default function UserGuideMain() {
interface UserGuideProps {
userGuideArticleCards: UserGuideArticlesProps[];
}
export default function UserGuideMain({
userGuideArticleCards,
}: UserGuideProps) {
return (
<StyledContainer>
<StyledWrapper>
@ -107,7 +111,7 @@ export default function UserGuideMain() {
</StyledSubHeading>
</StyledHeader>
<StyledContent>
{USER_GUIDE_HOME_CARDS.map((card) => {
{userGuideArticleCards.map((card) => {
return <UserGuideCard key={card.title} card={card} />;
})}
</StyledContent>

View File

@ -0,0 +1,46 @@
'use client';
import { ReactNode } 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 UserGuideTableContents from '@/app/_components/user-guide/TableContent';
import UserGuideSidebar from '@/app/_components/user-guide/UserGuideSidebar';
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
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 UserGuideMainLayout = ({
children,
userGuideIndex,
}: {
children: ReactNode;
userGuideIndex: UserGuideArticlesProps[];
}) => {
const pathname = usePathname();
return (
<StyledContainer>
<UserGuideSidebar userGuideIndex={userGuideIndex} />
{children}
{pathname === '/user-guide' ? (
<StyledEmptySideBar />
) : (
<UserGuideTableContents />
)}
</StyledContainer>
);
};

View File

@ -7,19 +7,24 @@ import { IconBook } from '@/app/_components/ui/icons';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
import UserGuideSidebarSection from '@/app/_components/user-guide/UserGuideSidebarSection';
import { USER_GUIDE_INDEX } from '@/content/user-guide/constants/UserGuideIndex';
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
const StyledContainer = styled.div`
${mq({
width: ['20%', '30%', '20%'],
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(3)}`,
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`
@ -52,8 +57,13 @@ const StyledHeadingText = styled.div`
color: ${Theme.text.color.secondary};
`;
const UserGuideSidebar = () => {
const UserGuideSidebar = ({
userGuideIndex,
}: {
userGuideIndex: UserGuideArticlesProps[];
}) => {
const router = useRouter();
return (
<StyledContainer>
<StyledHeading>
@ -64,13 +74,7 @@ const UserGuideSidebar = () => {
User Guide
</StyledHeadingText>
</StyledHeading>
{Object.entries(USER_GUIDE_INDEX).map(([heading, subtopics]) => (
<UserGuideSidebarSection
key={heading}
title={heading}
subTopics={subtopics}
/>
))}
<UserGuideSidebarSection userGuideIndex={userGuideIndex} />
</StyledContainer>
);
};

View File

@ -6,13 +6,18 @@ import { usePathname, useRouter } from 'next/navigation';
import { IconChevronDown, IconChevronRight } from '@/app/_components/ui/icons';
import { Theme } from '@/app/_components/ui/theme/theme';
import { IndexSubtopic } from '@/content/user-guide/constants/UserGuideIndex';
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
import { groupArticlesByTopic } from '@/content/user-guide/constants/groupArticlesByTopic';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledIndex = styled.div`
margin-bottom: 24px;
`;
const StyledTitle = styled.div`
cursor: pointer;
display: flex;
@ -70,53 +75,80 @@ const StyledIcon = styled.div`
align-items: center;
`;
const StyledRectangle = styled.div<{ isselected: boolean }>`
height: 100%;
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
: Theme.background.transparent.light};
: props.isHovered
? Theme.background.transparent.strong
: Theme.background.transparent.light};
transition: height 0.2s ease-in-out;
`;
interface TopicsState {
[topic: string]: boolean;
}
const UserGuideSidebarSection = ({
title,
subTopics,
userGuideIndex,
}: {
title: string;
subTopics: IndexSubtopic[];
userGuideIndex: UserGuideArticlesProps[];
}) => {
const [isUnfolded, setUnfoldedState] = useState(true);
const pathname = usePathname();
const router = useRouter();
const topics = groupArticlesByTopic(userGuideIndex);
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const [unfolded, setUnfolded] = useState<TopicsState>(() =>
Object.keys(topics).reduce((acc: TopicsState, topic: string) => {
acc[topic] = true;
return acc;
}, {}),
);
const toggleFold = (topic: string) => {
setUnfolded((prev: TopicsState) => ({ ...prev, [topic]: !prev[topic] }));
};
return (
<StyledContainer>
<StyledTitle onClick={() => setUnfoldedState(!isUnfolded)}>
{isUnfolded ? (
<StyledIcon>
<IconChevronDown size={Theme.icon.size.md} />
</StyledIcon>
) : (
<StyledIcon>
<IconChevronRight size={Theme.icon.size.md} />
</StyledIcon>
)}
<div>{title}</div>
</StyledTitle>
{isUnfolded &&
subTopics.map((subtopic, index) => {
const isselected = pathname === `/user-guide/${subtopic.url}`;
return (
<StyledSubTopicItem
key={index}
isselected={isselected}
onClick={() => router.push(`/user-guide/${subtopic.url}`)}
>
<StyledRectangle isselected={isselected} />
{subtopic.title}
</StyledSubTopicItem>
);
})}
{Object.entries(topics).map(([topic, cards]) => (
<StyledIndex key={topic}>
<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>
{unfolded[topic] &&
cards.map((card) => {
const isselected = pathname === `/user-guide/${card.fileName}`;
return (
<StyledSubTopicItem
key={card.title}
isselected={isselected}
onClick={() => router.push(`/user-guide/${card.fileName}`)}
onMouseEnter={() => setHoveredItem(card.title)}
onMouseLeave={() => setHoveredItem(null)}
>
<StyledRectangle
isselected={isselected}
isHovered={hoveredItem === card.title}
/>
{card.title}
</StyledSubTopicItem>
);
})}
</StyledIndex>
))}
</StyledContainer>
);
};

View File

@ -1,47 +0,0 @@
'use client';
import styled from '@emotion/styled';
import { useRouter } from 'next/navigation';
import mq from '@/app/_components/ui/theme/mq';
import { Theme } from '@/app/_components/ui/theme/theme';
const StyledContainer = styled.div`
${mq({
width: '20%',
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: `${Theme.spacing(10)} ${Theme.spacing(6)}`,
gap: `${Theme.spacing(6)}`,
'body nav': {
display: ['none', 'none', ''],
},
})};
`;
const StyledContent = styled.div`
position: fixed;
`;
const StyledHeadingText = styled.div`
font-size: ${Theme.font.size.sm};
color: ${Theme.text.color.quarternary};
`;
const UserGuideTableContents = () => {
const router = useRouter();
return (
<StyledContainer>
<StyledContent>
<StyledHeadingText onClick={() => router.push('/user-guide')}>
Table of Content
</StyledHeadingText>
</StyledContent>
</StyledContainer>
);
};
export default UserGuideTableContents;

View File

@ -1,21 +0,0 @@
interface Heading {
id: string;
value: string;
}
const UserGuideTocComponent = ({ headings }: { headings: Heading[] }) => {
return (
<div>
<h2>Table of Contents</h2>
<ul>
{headings.map((heading, index) => (
<li key={index}>
<a href={`#${heading.id}`}>{heading.value}</a>
</li>
))}
</ul>
</div>
);
};
export default UserGuideTocComponent;

View File

@ -1,9 +1,7 @@
import { ReactElement } from 'react';
import { toc } from '@jsdevtools/rehype-toc';
import fs from 'fs';
import { compileMDX } from 'next-mdx-remote/rsc';
import path from 'path';
import rehypeSlug from 'rehype-slug';
import gfm from 'remark-gfm';
import ArticleEditContent from '@/app/_components/ui/layout/articles/ArticleEditContent';
@ -103,7 +101,7 @@ async function parseFrontMatterAndCategory(
return parsedDirectory;
}
export async function compileMDXFile(filePath: string, addToc = true) {
export async function compileMDXFile(filePath: string) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const compiled = await compileMDX<{ title: string; position?: number }>({
source: fileContent,
@ -123,7 +121,6 @@ export async function compileMDXFile(filePath: string, addToc = true) {
mdxOptions: {
development: process.env.NODE_ENV === 'development',
remarkPlugins: [gfm],
rehypePlugins: [rehypeSlug, ...(addToc ? [toc] : [])],
},
},
});
@ -147,7 +144,7 @@ export async function getPost(
if (!fs.existsSync(filePath)) {
return null;
}
const { content, frontmatter } = await compileMDXFile(filePath, true);
const { content, frontmatter } = await compileMDXFile(filePath);
return {
content,

View File

@ -0,0 +1,25 @@
import { useEffect, useRef, useState } from 'react';
export function useHeadsObserver(location: string) {
const [activeText, setActiveText] = useState('');
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
const handleObsever = (entries: any[]) => {
entries.forEach((entry) => {
if (entry?.isIntersecting) {
setActiveText(entry.target.innerText);
}
});
};
observer.current = new IntersectionObserver(handleObsever, {
rootMargin: '0% 0% -85% 0px',
});
const elements = document.querySelectorAll('h2, h3, h4, h5');
elements.forEach((elem) => observer.current?.observe(elem));
return () => observer.current?.disconnect();
}, [location]);
return { activeText };
}

View File

@ -1,38 +1,13 @@
'use client';
import { ReactNode } 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 UserGuideSidebar from '@/app/_components/user-guide/UserGuideSidebar';
import UserGuideTableContents from '@/app/_components/user-guide/UserGuideTableContents';
const StyledContainer = styled.div`
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between:
border-bottom: 1px solid ${Theme.background.transparent.medium};
`;
const StyledEmptySideBar = styled.div`
${mq({
width: '20%',
display: ['none', 'none', ''],
})};
`;
import { UserGuideMainLayout } from '@/app/_components/user-guide/UserGuideMainLayout';
import { getUserGuideArticles } from '@/content/user-guide/constants/getUserGuideArticles';
export default function UserGuideLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();
const userGuideIndex = getUserGuideArticles();
return (
<StyledContainer>
<UserGuideSidebar />
<UserGuideMainLayout userGuideIndex={userGuideIndex}>
{children}
{pathname === '/user-guide' ? (
<StyledEmptySideBar />
) : (
<UserGuideTableContents />
)}
</StyledContainer>
</UserGuideMainLayout>
);
}

View File

@ -1,4 +1,5 @@
import UserGuideMain from '@/app/_components/user-guide/UserGuideMain';
import { getUserGuideArticles } from '@/content/user-guide/constants/getUserGuideArticles';
export const metadata = {
title: 'Twenty - User Guide',
@ -10,5 +11,7 @@ export const metadata = {
export const dynamic = 'force-dynamic';
export default async function UserGuideHome() {
return <UserGuideMain />;
const userGuideArticleCards = getUserGuideArticles();
return <UserGuideMain userGuideArticleCards={userGuideArticleCards} />;
}

View File

@ -1,34 +0,0 @@
export type UserGuideHomeCardsType = {
url: string;
title: string;
subtitle: string;
image: string;
};
export const USER_GUIDE_HOME_CARDS: UserGuideHomeCardsType[] = [
{
url: 'what-is-twenty',
title: 'What is Twenty',
subtitle:
"A brief on Twenty's commitment to reshaping CRM with Open Source",
image: '/images/user-guide/home/what-is-twenty.png',
},
{
url: 'create-workspace',
title: 'Create a Workspace',
subtitle: 'Custom objects store unique info in workspaces.',
image: '/images/user-guide/home/create-a-workspace.png',
},
{
url: 'import-export-data',
title: 'Import your data',
subtitle: 'Easily create a note to keep track of important information.',
image: '/images/user-guide/home/import-your-data.png',
},
{
url: 'objects',
title: 'Custom Objects',
subtitle: 'Custom objects store unique info in workspaces.',
image: '/images/user-guide/home/custom-objects.png',
},
];

View File

@ -1,35 +1,26 @@
export type IndexSubtopic = {
title: string;
url: string;
};
export type IndexHeading = {
[heading: string]: IndexSubtopic[];
};
export const USER_GUIDE_INDEX = {
'Getting Started': [
{ title: 'What is Twenty', url: 'what-is-twenty' },
{ title: 'Create a Workspace', url: 'create-workspace' },
{ fileName: 'what-is-twenty' },
{ fileName: 'create-workspace' },
],
Objects: [
{ title: 'Objects', url: 'objects' },
{ title: 'Fields', url: 'fields' },
{ title: 'Views, Sort and Filter', url: 'views-sort-filter' },
{ title: 'Table Views', url: 'table-views' },
{ title: 'Kanban Views', url: 'kanban-views' },
{ title: 'Import/Export Data', url: 'import-export-data' },
{ fileName: 'objects' },
{ fileName: 'fields' },
{ fileName: 'views-sort-filter' },
{ fileName: 'table-views' },
{ fileName: 'kanban-views' },
{ fileName: 'import-export-data' },
],
Functions: [
{ title: 'Emails', url: 'emails' },
{ title: 'Notes', url: 'notes' },
{ title: 'Tasks', url: 'tasks' },
{ title: 'Integrations', url: 'integrations' },
{ title: 'API and Webhooks', url: 'api-webhooks' },
{ fileName: 'emails' },
{ fileName: 'notes' },
{ fileName: 'tasks' },
{ fileName: 'integrations' },
{ fileName: 'api-webhooks' },
],
Other: [
{ title: 'Glossary', url: 'glossary' },
{ title: 'Tips', url: 'tips' },
{ title: 'Github', url: 'github' },
{ fileName: 'glossary' },
{ fileName: 'tips' },
{ fileName: 'github' },
],
};

View File

@ -0,0 +1,36 @@
import fs from 'fs';
import matter from 'gray-matter';
import { USER_GUIDE_INDEX } from '@/content/user-guide/constants/UserGuideIndex';
export interface UserGuideArticlesProps {
title: string;
info: string;
image: string;
fileName: string;
topic: string;
}
export function getUserGuideArticles() {
const guides: UserGuideArticlesProps[] = [];
for (const [topic, files] of Object.entries(USER_GUIDE_INDEX)) {
files.forEach(({ fileName }) => {
const filePath = `src/content/user-guide/${fileName}.mdx`;
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const { data: frontmatter } = matter(fileContent);
guides.push({
title: frontmatter.title || '',
info: frontmatter.info || '',
image: frontmatter.image || '',
fileName: fileName,
topic: topic,
});
}
});
}
return guides;
}

View File

@ -0,0 +1,14 @@
import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles';
export const groupArticlesByTopic = (
items: UserGuideArticlesProps[],
): Record<string, UserGuideArticlesProps[]> => {
return items.reduce(
(acc, item) => {
acc[item.topic] = acc[item.topic] || [];
acc[item.topic].push(item);
return acc;
},
{} as Record<string, UserGuideArticlesProps[]>,
);
};