mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-27 11:03:40 +03:00
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:
parent
df5cb9a904
commit
0bc3b6f179
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
|
@ -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 };
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
@ -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' },
|
||||
],
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
@ -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[]>,
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user