From a4b2321553367a32b5723721a70e718887d8b6d4 Mon Sep 17 00:00:00 2001 From: Ady Beraud <102751374+ady-beraud@users.noreply.github.com> Date: Wed, 1 May 2024 09:55:57 +0300 Subject: [PATCH] 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 --- .../app/_components/contributors/Header.tsx | 15 +- .../app/_components/oss-friends/Header.tsx | 18 ++- .../app/_components/releases/StyledTitle.tsx | 15 +- .../_components/ui/layout/FooterDesktop.tsx | 2 - .../ui/layout/articles/ArticleEditContent.tsx | 4 +- .../_components/user-guide/TableContent.tsx | 149 ++++++++++++++++++ .../_components/user-guide/UserGuideCard.tsx | 23 +-- .../user-guide/UserGuideContent.tsx | 54 +++---- .../_components/user-guide/UserGuideMain.tsx | 42 ++--- .../user-guide/UserGuideMainLayout.tsx | 46 ++++++ .../user-guide/UserGuideSidebar.tsx | 28 ++-- .../user-guide/UserGuideSidebarSection.tsx | 102 ++++++++---- .../user-guide/UserGuideTableContents.tsx | 47 ------ .../user-guide/UserGuideTocComponent.tsx | 21 --- .../src/app/_server-utils/get-posts.tsx | 7 +- .../app/user-guide/hooks/useHeadsObserver.tsx | 25 +++ .../src/app/user-guide/layout.tsx | 35 +--- .../src/app/user-guide/page.tsx | 5 +- .../constants/UserGuideHomeCards.ts | 34 ---- .../user-guide/constants/UserGuideIndex.ts | 41 ++--- .../constants/getUserGuideArticles.tsx | 36 +++++ .../constants/groupArticlesByTopic.tsx | 14 ++ 22 files changed, 479 insertions(+), 284 deletions(-) create mode 100644 packages/twenty-website/src/app/_components/user-guide/TableContent.tsx create mode 100644 packages/twenty-website/src/app/_components/user-guide/UserGuideMainLayout.tsx delete mode 100644 packages/twenty-website/src/app/_components/user-guide/UserGuideTableContents.tsx delete mode 100644 packages/twenty-website/src/app/_components/user-guide/UserGuideTocComponent.tsx create mode 100644 packages/twenty-website/src/app/user-guide/hooks/useHeadsObserver.tsx delete mode 100644 packages/twenty-website/src/content/user-guide/constants/UserGuideHomeCards.ts create mode 100644 packages/twenty-website/src/content/user-guide/constants/getUserGuideArticles.tsx create mode 100644 packages/twenty-website/src/content/user-guide/constants/groupArticlesByTopic.tsx diff --git a/packages/twenty-website/src/app/_components/contributors/Header.tsx b/packages/twenty-website/src/app/_components/contributors/Header.tsx index 9b2bf3b7318a..fee05146765a 100644 --- a/packages/twenty-website/src/app/_components/contributors/Header.tsx +++ b/packages/twenty-website/src/app/_components/contributors/Header.tsx @@ -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 ( <> - - Our amazing <br /> - <span style={{ color: '#141414' }}>Contributors</span> - + + + Our amazing <br /> + <span style={{ color: '#141414' }}>Contributors</span> + + ); }; diff --git a/packages/twenty-website/src/app/_components/oss-friends/Header.tsx b/packages/twenty-website/src/app/_components/oss-friends/Header.tsx index 9086aada426e..32260eac4b8a 100644 --- a/packages/twenty-website/src/app/_components/oss-friends/Header.tsx +++ b/packages/twenty-website/src/app/_components/oss-friends/Header.tsx @@ -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 ( -
- - Open-source <br /> <span style={{ color: 'black' }}>friends</span> - - + <> + + + Open-source <br /> <span style={{ color: 'black' }}>friends</span> + + We are proud to collaborate with a diverse group of partners to promote open-source software. -
+ ); }; diff --git a/packages/twenty-website/src/app/_components/releases/StyledTitle.tsx b/packages/twenty-website/src/app/_components/releases/StyledTitle.tsx index 04c045aa3c57..32a84c482ed0 100644 --- a/packages/twenty-website/src/app/_components/releases/StyledTitle.tsx +++ b/packages/twenty-website/src/app/_components/releases/StyledTitle.tsx @@ -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 ( - - Latest - Releases - + + + Latest + Releases + + ); }; diff --git a/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx b/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx index 2f1bbe863dcd..38715c8f412d 100644 --- a/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx +++ b/packages/twenty-website/src/app/_components/ui/layout/FooterDesktop.tsx @@ -77,7 +77,6 @@ export const FooterDesktop = () => {
{
diff --git a/packages/twenty-website/src/app/_components/user-guide/TableContent.tsx b/packages/twenty-website/src/app/_components/user-guide/TableContent.tsx new file mode 100644 index 000000000000..d11bfeb346eb --- /dev/null +++ b/packages/twenty-website/src/app/_components/user-guide/TableContent.tsx @@ -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([]); + 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 ( + + + Table of Content + + {headings.map((heading) => ( + + { + 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} + + + ))} + + + + ); +}; + +export default UserGuideTableContents; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideCard.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideCard.tsx index 1984a174b460..5b32e88a9b02 100644 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideCard.tsx +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideCard.tsx @@ -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 ( - router.push(`/user-guide/${card.url}`)}> + router.push(`/user-guide/${card.fileName}`)} + > {card.title} - {card.subtitle} + {card.info} ); } diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideContent.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideContent.tsx index 18515168ae76..d553040cbaf7 100644 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideContent.tsx +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideContent.tsx @@ -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; } } `; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideMain.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideMain.tsx index 1c30d935354f..7149076390d9 100644 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideMain.tsx +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideMain.tsx @@ -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 ( @@ -107,7 +111,7 @@ export default function UserGuideMain() { - {USER_GUIDE_HOME_CARDS.map((card) => { + {userGuideArticleCards.map((card) => { return ; })} diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideMainLayout.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideMainLayout.tsx new file mode 100644 index 000000000000..dc661153e361 --- /dev/null +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideMainLayout.tsx @@ -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 ( + + + {children} + {pathname === '/user-guide' ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebar.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebar.tsx index 2d798f2476e9..9d20baa01de4 100644 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebar.tsx +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebar.tsx @@ -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 ( @@ -64,13 +74,7 @@ const UserGuideSidebar = () => { User Guide - {Object.entries(USER_GUIDE_INDEX).map(([heading, subtopics]) => ( - - ))} + ); }; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebarSection.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebarSection.tsx index 57c5323cfa51..79f42929052e 100644 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebarSection.tsx +++ b/packages/twenty-website/src/app/_components/user-guide/UserGuideSidebarSection.tsx @@ -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(null); + + const [unfolded, setUnfolded] = useState(() => + 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 ( - setUnfoldedState(!isUnfolded)}> - {isUnfolded ? ( - - - - ) : ( - - - - )} -
{title}
-
- {isUnfolded && - subTopics.map((subtopic, index) => { - const isselected = pathname === `/user-guide/${subtopic.url}`; - return ( - router.push(`/user-guide/${subtopic.url}`)} - > - - {subtopic.title} - - ); - })} + {Object.entries(topics).map(([topic, cards]) => ( + + toggleFold(topic)}> + {unfolded[topic] ? ( + + + + ) : ( + + + + )} +
{topic}
+
+ {unfolded[topic] && + cards.map((card) => { + const isselected = pathname === `/user-guide/${card.fileName}`; + return ( + router.push(`/user-guide/${card.fileName}`)} + onMouseEnter={() => setHoveredItem(card.title)} + onMouseLeave={() => setHoveredItem(null)} + > + + {card.title} + + ); + })} +
+ ))}
); }; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideTableContents.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideTableContents.tsx deleted file mode 100644 index 7e03abc4d51e..000000000000 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideTableContents.tsx +++ /dev/null @@ -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 ( - - - router.push('/user-guide')}> - Table of Content - - - - ); -}; - -export default UserGuideTableContents; diff --git a/packages/twenty-website/src/app/_components/user-guide/UserGuideTocComponent.tsx b/packages/twenty-website/src/app/_components/user-guide/UserGuideTocComponent.tsx deleted file mode 100644 index 28160311c2d5..000000000000 --- a/packages/twenty-website/src/app/_components/user-guide/UserGuideTocComponent.tsx +++ /dev/null @@ -1,21 +0,0 @@ -interface Heading { - id: string; - value: string; -} - -const UserGuideTocComponent = ({ headings }: { headings: Heading[] }) => { - return ( -
-

Table of Contents

- -
- ); -}; - -export default UserGuideTocComponent; diff --git a/packages/twenty-website/src/app/_server-utils/get-posts.tsx b/packages/twenty-website/src/app/_server-utils/get-posts.tsx index fec2ae72feda..d05ce9837e34 100644 --- a/packages/twenty-website/src/app/_server-utils/get-posts.tsx +++ b/packages/twenty-website/src/app/_server-utils/get-posts.tsx @@ -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, diff --git a/packages/twenty-website/src/app/user-guide/hooks/useHeadsObserver.tsx b/packages/twenty-website/src/app/user-guide/hooks/useHeadsObserver.tsx new file mode 100644 index 000000000000..24ee7ae267bd --- /dev/null +++ b/packages/twenty-website/src/app/user-guide/hooks/useHeadsObserver.tsx @@ -0,0 +1,25 @@ +import { useEffect, useRef, useState } from 'react'; + +export function useHeadsObserver(location: string) { + const [activeText, setActiveText] = useState(''); + const observer = useRef(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 }; +} diff --git a/packages/twenty-website/src/app/user-guide/layout.tsx b/packages/twenty-website/src/app/user-guide/layout.tsx index 163e36385144..b9af90a52ad0 100644 --- a/packages/twenty-website/src/app/user-guide/layout.tsx +++ b/packages/twenty-website/src/app/user-guide/layout.tsx @@ -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 ( - - + {children} - {pathname === '/user-guide' ? ( - - ) : ( - - )} - + ); } diff --git a/packages/twenty-website/src/app/user-guide/page.tsx b/packages/twenty-website/src/app/user-guide/page.tsx index c847ddaba3f8..665cb5296558 100644 --- a/packages/twenty-website/src/app/user-guide/page.tsx +++ b/packages/twenty-website/src/app/user-guide/page.tsx @@ -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 ; + const userGuideArticleCards = getUserGuideArticles(); + + return ; } diff --git a/packages/twenty-website/src/content/user-guide/constants/UserGuideHomeCards.ts b/packages/twenty-website/src/content/user-guide/constants/UserGuideHomeCards.ts deleted file mode 100644 index df083476e04b..000000000000 --- a/packages/twenty-website/src/content/user-guide/constants/UserGuideHomeCards.ts +++ /dev/null @@ -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', - }, -]; diff --git a/packages/twenty-website/src/content/user-guide/constants/UserGuideIndex.ts b/packages/twenty-website/src/content/user-guide/constants/UserGuideIndex.ts index 1a392ff16006..2401ffc5f04e 100644 --- a/packages/twenty-website/src/content/user-guide/constants/UserGuideIndex.ts +++ b/packages/twenty-website/src/content/user-guide/constants/UserGuideIndex.ts @@ -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' }, ], }; diff --git a/packages/twenty-website/src/content/user-guide/constants/getUserGuideArticles.tsx b/packages/twenty-website/src/content/user-guide/constants/getUserGuideArticles.tsx new file mode 100644 index 000000000000..d101f7f1f0fa --- /dev/null +++ b/packages/twenty-website/src/content/user-guide/constants/getUserGuideArticles.tsx @@ -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; +} diff --git a/packages/twenty-website/src/content/user-guide/constants/groupArticlesByTopic.tsx b/packages/twenty-website/src/content/user-guide/constants/groupArticlesByTopic.tsx new file mode 100644 index 000000000000..9a9029e01582 --- /dev/null +++ b/packages/twenty-website/src/content/user-guide/constants/groupArticlesByTopic.tsx @@ -0,0 +1,14 @@ +import { UserGuideArticlesProps } from '@/content/user-guide/constants/getUserGuideArticles'; + +export const groupArticlesByTopic = ( + items: UserGuideArticlesProps[], +): Record => { + return items.reduce( + (acc, item) => { + acc[item.topic] = acc[item.topic] || []; + acc[item.topic].push(item); + return acc; + }, + {} as Record, + ); +};