WIP: New User Guide (#3984)
* initial commit * Theme setup on twenty-website package * Left bar, Content done * Content added, useDeviceType hook added * useDeviceType file renamed * Responsiveness introduced * Mobile responsiveness fix * TOC layout * PR fixes * PR changes 2 * PR changes #3
This commit is contained in:
@ -1,16 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronLeft } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { DeviceType, useDeviceType } from '@/app/ui/utilities/useDeviceType';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: ${Theme.spacing(2)};
|
||||
color: #b3b3b3;
|
||||
`;
|
||||
|
||||
const InternalLinkItem = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: #b3b3b3;
|
||||
&:hover {
|
||||
color: ${Theme.text.color.quarternary};
|
||||
}
|
||||
`;
|
||||
|
||||
const ExternalLinkItem = styled.a`
|
||||
@ -19,7 +28,26 @@ const ExternalLinkItem = styled.a`
|
||||
`;
|
||||
|
||||
const ActivePage = styled.span`
|
||||
color: #818181;
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledSection = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${Theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledMobileContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(1)};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
`;
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
@ -37,17 +65,29 @@ export const Breadcrumbs = ({
|
||||
activePage,
|
||||
separator,
|
||||
}: BreadcrumbsProps) => {
|
||||
const isMobile = useDeviceType() === DeviceType.MOBILE;
|
||||
if (isMobile) {
|
||||
const lastItem = items[items.length - 1];
|
||||
return (
|
||||
<StyledMobileContainer>
|
||||
<IconChevronLeft size={Theme.icon.size.md} />
|
||||
<InternalLinkItem href={lastItem.uri}>
|
||||
{lastItem.label}
|
||||
</InternalLinkItem>
|
||||
</StyledMobileContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={`${item?.uri ?? 'item'}-${index}`}>
|
||||
<StyledSection key={`${item?.uri ?? 'item'}-${index}`}>
|
||||
{item.isExternal ? (
|
||||
<ExternalLinkItem href={item.uri}>{item.label}</ExternalLinkItem>
|
||||
) : (
|
||||
<InternalLinkItem href={item.uri}>{item.label}</InternalLinkItem>
|
||||
)}
|
||||
<span>{separator}</span>
|
||||
</React.Fragment>
|
||||
<div>{separator}</div>
|
||||
</StyledSection>
|
||||
))}
|
||||
<ActivePage>{activePage}</ActivePage>
|
||||
</Container>
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { UserGuideHomeCardsType } from '@/app/user-guide/constants/UserGuideHomeCards';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
color: ${Theme.border.color.plain};
|
||||
border: 2px solid ${Theme.border.color.plain};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
padding: ${Theme.spacing(6)};
|
||||
gap: ${Theme.spacing(4)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
width: 348px;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
font-size: ${Theme.font.size.lg};
|
||||
color: ${Theme.text.color.primary};
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-family: ${Theme.font.family};
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img`
|
||||
width: 300px;
|
||||
`;
|
||||
|
||||
export default function UserGuideCard({
|
||||
card,
|
||||
}: {
|
||||
card: UserGuideHomeCardsType;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<StyledContainer onClick={() => router.push(`/user-guide/${card.url}`)}>
|
||||
<StyledImage src={card.image} alt={card.title} />
|
||||
<StyledHeading>{card.title}</StyledHeading>
|
||||
<StyledSubHeading>{card.subtitle}</StyledSubHeading>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Breadcrumbs } from '@/app/components/Breadcrumbs';
|
||||
import { FileContent } from '@/app/get-posts';
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { DeviceType, useDeviceType } from '@/app/ui/utilities/useDeviceType';
|
||||
|
||||
const StyledContainer = styled.div<{ devicetype: string }>`
|
||||
width: ${({ devicetype }) =>
|
||||
devicetype === DeviceType.TABLET
|
||||
? '70%'
|
||||
: devicetype === DeviceType.DESKTOP
|
||||
? '60%'
|
||||
: '100%'};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-family: ${Theme.font.family};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 79.3%;
|
||||
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)} 0px;
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionTitle = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
padding: ${Theme.spacing(2)} 0px;
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionSub = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(4)};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
font-family: ${Theme.font.family};
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: ${Theme.background.transparent.medium};
|
||||
`;
|
||||
|
||||
export default function UserGuideContent({ item }: { item: FileContent }) {
|
||||
const BREADCRUMB_ITEMS = [
|
||||
{
|
||||
uri: '/user-guide',
|
||||
label: 'User Guide',
|
||||
},
|
||||
];
|
||||
const deviceType = useDeviceType();
|
||||
return (
|
||||
<StyledContainer devicetype={deviceType}>
|
||||
<StyledWrapper>
|
||||
<StyledHeader>
|
||||
<Breadcrumbs
|
||||
items={BREADCRUMB_ITEMS}
|
||||
activePage={item.itemInfo.title}
|
||||
separator="/"
|
||||
/>
|
||||
<StyledHeading>{item.itemInfo.title}</StyledHeading>
|
||||
{item.itemInfo.image && (
|
||||
<img
|
||||
id={`img-${item.itemInfo.title}`}
|
||||
src={item.itemInfo.image}
|
||||
alt={item.itemInfo.title}
|
||||
/>
|
||||
)}
|
||||
<StyledHeaderInfoSection>
|
||||
<StyledHeaderInfoSectionTitle>
|
||||
In this article
|
||||
</StyledHeaderInfoSectionTitle>
|
||||
<StyledHeaderInfoSectionSub>
|
||||
{item.itemInfo.info}
|
||||
</StyledHeaderInfoSectionSub>
|
||||
</StyledHeaderInfoSection>
|
||||
<StyledRectangle />
|
||||
</StyledHeader>
|
||||
<div>{item.content}</div>
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import UserGuideCard from '@/app/components/user-guide/UserGuideCard';
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { DeviceType, useDeviceType } from '@/app/ui/utilities/useDeviceType';
|
||||
import { UserGuideHomeCards } from '@/app/user-guide/constants/UserGuideHomeCards';
|
||||
|
||||
const StyledContainer = styled.div<{ isMobile: boolean }>`
|
||||
width: ${({ isMobile }) => (isMobile ? '100%' : '60%')};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 79.3%;
|
||||
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)} 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.h1`
|
||||
line-height: 38px;
|
||||
font-weight: 700;
|
||||
font-size: 38px;
|
||||
color: ${Theme.text.color.primary};
|
||||
margin: 0px;
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.h1`
|
||||
line-height: 12px;
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.regular};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledContentGrid = styled.div`
|
||||
width: 100%;
|
||||
padding-top: ${Theme.spacing(6)};
|
||||
display: grid;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto auto;
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledContentFlex = styled.div`
|
||||
width: 100%;
|
||||
padding-top: ${Theme.spacing(6)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
export default function UserGuideMain() {
|
||||
const deviceType = useDeviceType();
|
||||
return (
|
||||
<StyledContainer isMobile={deviceType === DeviceType.MOBILE}>
|
||||
<StyledWrapper>
|
||||
<StyledHeader>
|
||||
<StyledHeading>User Guide</StyledHeading>
|
||||
<StyledSubHeading>
|
||||
A brief guide to grasp the basics of Twenty
|
||||
</StyledSubHeading>
|
||||
</StyledHeader>
|
||||
{deviceType === DeviceType.DESKTOP ? (
|
||||
<StyledContentGrid>
|
||||
{UserGuideHomeCards.map((card) => {
|
||||
return <UserGuideCard key={card.title} card={card} />;
|
||||
})}
|
||||
</StyledContentGrid>
|
||||
) : (
|
||||
<StyledContentFlex>
|
||||
{UserGuideHomeCards.map((card) => {
|
||||
return <UserGuideCard key={card.title} card={card} />;
|
||||
})}
|
||||
</StyledContentFlex>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import UserGuideSidebarSection from '@/app/components/user-guide/UserGuideSidebarSection';
|
||||
import { IconBook } from '@/app/ui/icons';
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { DeviceType, useDeviceType } from '@/app/ui/utilities/useDeviceType';
|
||||
import { UserGuideIndex } from '@/app/user-guide/constants/UserGuideIndex';
|
||||
|
||||
const StyledContainer = styled.div<{ isTablet: boolean }>`
|
||||
width: ${({ isTablet }) => (isTablet ? '30%' : '20%')};
|
||||
background: ${Theme.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid ${Theme.background.transparent.medium};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
padding: ${Theme.spacing(10)} ${Theme.spacing(3)};
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.secondary};
|
||||
border: 1px solid ${Theme.text.color.secondary};
|
||||
border-radius: ${Theme.border.radius.sm};
|
||||
padding: ${Theme.spacing(1)} ${Theme.spacing(1)} ${Theme.spacing(1)}
|
||||
${Theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledHeadingText = styled.div`
|
||||
cursor: pointer;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const UserGuideSidebar = () => {
|
||||
const router = useRouter();
|
||||
const isTablet = useDeviceType() === DeviceType.TABLET;
|
||||
return (
|
||||
<StyledContainer isTablet={isTablet}>
|
||||
<StyledHeading>
|
||||
<StyledIconContainer>
|
||||
<IconBook size={Theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<StyledHeadingText onClick={() => router.push('/user-guide')}>
|
||||
User Guide
|
||||
</StyledHeadingText>
|
||||
</StyledHeading>
|
||||
{Object.entries(UserGuideIndex).map(([heading, subtopics]) => (
|
||||
<UserGuideSidebarSection
|
||||
key={heading}
|
||||
title={heading}
|
||||
subTopics={subtopics}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideSidebar;
|
||||
@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { IconChevronDown, IconChevronRight } from '@/app/ui/icons';
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { IndexSubtopic } from '@/app/user-guide/constants/UserGuideIndex';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
padding-bottom: ${Theme.spacing(2)};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
`;
|
||||
|
||||
const StyledSubTopicItem = styled.div<{ isselected: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: ${Theme.spacing(8)};
|
||||
color: ${(props) =>
|
||||
props.isselected ? Theme.text.color.primary : Theme.text.color.secondary};
|
||||
font-weight: ${(props) =>
|
||||
props.isselected ? Theme.font.weight.medium : Theme.font.weight.regular};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
gap: 19px;
|
||||
padding: ${(props) =>
|
||||
props.isselected ? '6px 12px 6px 11px' : '0px 12px 0px 11px'};
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.background.transparent.light
|
||||
: Theme.background.secondary};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
text-decoration: none;
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:visited,
|
||||
&:link,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
padding: 0px 4px 0px 4px;
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div<{ isselected: boolean }>`
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.border.color.plain
|
||||
: Theme.background.transparent.light};
|
||||
`;
|
||||
|
||||
const UserGuideSidebarSection = ({
|
||||
title,
|
||||
subTopics,
|
||||
}: {
|
||||
title: string;
|
||||
subTopics: IndexSubtopic[];
|
||||
}) => {
|
||||
const [isUnfolded, setUnfoldedState] = useState(true);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideSidebarSection;
|
||||
@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 20%;
|
||||
background: ${Theme.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid ${Theme.background.transparent.medium};
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
padding: ${Theme.spacing(10)} ${Theme.spacing(6)};
|
||||
gap: ${Theme.spacing(6)};
|
||||
`;
|
||||
|
||||
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 Contents
|
||||
</StyledHeadingText>
|
||||
</StyledContent>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserGuideTableContents;
|
||||
@ -0,0 +1,21 @@
|
||||
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,5 +1,5 @@
|
||||
import { ReactElement } from 'react';
|
||||
import rehypeToc from '@jsdevtools/rehype-toc';
|
||||
import { toc } from '@jsdevtools/rehype-toc';
|
||||
import fs from 'fs';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import path from 'path';
|
||||
@ -12,6 +12,8 @@ interface ItemInfo {
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
icon?: string;
|
||||
info?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
@ -99,14 +101,13 @@ async function parseFrontMatterAndCategory(
|
||||
|
||||
export async function compileMDXFile(filePath: string, addToc: boolean = true) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const compiled = await compileMDX<{ title: string; position?: number }>({
|
||||
source: fileContent,
|
||||
options: {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [rehypeSlug, ...(addToc ? [rehypeToc] : [])],
|
||||
rehypePlugins: [rehypeSlug, ...(addToc ? [toc] : [])],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -121,21 +122,19 @@ export async function getPosts(basePath: string): Promise<Directory> {
|
||||
}
|
||||
|
||||
export async function getPost(
|
||||
slug: string[],
|
||||
slug: string,
|
||||
basePath: string,
|
||||
): Promise<FileContent | null> {
|
||||
const postsDirectory = path.join(process.cwd(), basePath);
|
||||
const modifiedSlug = slug.join('/');
|
||||
const filePath = path.join(postsDirectory, `${modifiedSlug}.mdx`);
|
||||
const filePath = path.join(postsDirectory, `${slug}.mdx`);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { content, frontmatter } = await compileMDXFile(filePath);
|
||||
const { content, frontmatter } = await compileMDXFile(filePath, true);
|
||||
|
||||
return {
|
||||
content,
|
||||
itemInfo: { ...frontmatter, type: 'file', path: modifiedSlug },
|
||||
itemInfo: { ...frontmatter, type: 'file', path: slug },
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,8 +30,24 @@ a {
|
||||
}
|
||||
|
||||
nav.toc {
|
||||
width: 200px;
|
||||
width: 20%;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
top: 125px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
nav.toc ol{
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
nav.toc li{
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
nav.toc a{
|
||||
color: #141414;
|
||||
font-family: var(--font-inter);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { Metadata } from 'next';
|
||||
import { Gabarito } from 'next/font/google';
|
||||
import { Gabarito, Inter } from 'next/font/google';
|
||||
|
||||
import { HeaderMobile } from '@/app/components/HeaderMobile';
|
||||
|
||||
@ -16,10 +16,19 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
const gabarito = Gabarito({
|
||||
weight: ['400', '500'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
adjustFontFallback: false,
|
||||
variable: '--font-gabarito',
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
adjustFontFallback: false,
|
||||
variable: '--font-inter',
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
@ -28,7 +37,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={gabarito.className}>
|
||||
<html lang="en" className={`${gabarito.className} ${inter.className}`}>
|
||||
<body>
|
||||
<EmotionRootStyleRegistry>
|
||||
<HeaderDesktop />
|
||||
|
||||
7
packages/twenty-website/src/app/ui/icons/index.ts
Normal file
7
packages/twenty-website/src/app/ui/icons/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type { TablerIconsProps } from '@tabler/icons-react';
|
||||
export {
|
||||
IconBook,
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
} from '@tabler/icons-react';
|
||||
13
packages/twenty-website/src/app/ui/theme/background.ts
Normal file
13
packages/twenty-website/src/app/ui/theme/background.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Color, rgba } from './colors';
|
||||
|
||||
export const Background = {
|
||||
primary: Color.white,
|
||||
secondary: Color.gray10,
|
||||
tertiary: Color.gray20,
|
||||
transparent: {
|
||||
strong: rgba(Color.gray60, 0.16),
|
||||
medium: rgba(Color.gray60, 0.08),
|
||||
light: rgba(Color.gray60, 0.06),
|
||||
lighter: rgba(Color.gray60, 0.04),
|
||||
},
|
||||
};
|
||||
19
packages/twenty-website/src/app/ui/theme/border.ts
Normal file
19
packages/twenty-website/src/app/ui/theme/border.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Color } from './colors';
|
||||
|
||||
const common = {
|
||||
radius: {
|
||||
xs: '2px',
|
||||
sm: '4px',
|
||||
md: '8px',
|
||||
xl: '20px',
|
||||
pill: '999px',
|
||||
rounded: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
export const Border = {
|
||||
color: {
|
||||
plain: Color.gray60,
|
||||
},
|
||||
...common,
|
||||
};
|
||||
24
packages/twenty-website/src/app/ui/theme/colors.ts
Normal file
24
packages/twenty-website/src/app/ui/theme/colors.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import hexRgb from 'hex-rgb';
|
||||
|
||||
export const mainColors = {
|
||||
white: '#ffffff',
|
||||
};
|
||||
|
||||
export const secondaryColors = {
|
||||
gray60: '#141414',
|
||||
gray50: '#474747',
|
||||
gray40: '#818181',
|
||||
gray30: '#b3b3b3',
|
||||
gray20: '#f1f1f1',
|
||||
gray10: '#fafafa',
|
||||
};
|
||||
|
||||
export const Color = {
|
||||
...mainColors,
|
||||
...secondaryColors,
|
||||
};
|
||||
|
||||
export const rgba = (hex: string, alpha: number) => {
|
||||
const rgb = hexRgb(hex, { format: 'array' }).slice(0, -1).join(',');
|
||||
return `rgba(${rgb},${alpha})`;
|
||||
};
|
||||
14
packages/twenty-website/src/app/ui/theme/font.ts
Normal file
14
packages/twenty-website/src/app/ui/theme/font.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const Font = {
|
||||
size: {
|
||||
xs: '0.875rem',
|
||||
sm: '1rem',
|
||||
base: '1.125rem',
|
||||
lg: '1.25rem',
|
||||
xl: '1.5rem',
|
||||
},
|
||||
weight: {
|
||||
regular: 400,
|
||||
medium: 500,
|
||||
},
|
||||
family: 'Inter, sans-serif',
|
||||
};
|
||||
13
packages/twenty-website/src/app/ui/theme/icon.ts
Normal file
13
packages/twenty-website/src/app/ui/theme/icon.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const Icon = {
|
||||
size: {
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 40,
|
||||
},
|
||||
stroke: {
|
||||
sm: 1.6,
|
||||
md: 2,
|
||||
lg: 2.5,
|
||||
},
|
||||
};
|
||||
22
packages/twenty-website/src/app/ui/theme/text.ts
Normal file
22
packages/twenty-website/src/app/ui/theme/text.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Color } from './colors';
|
||||
|
||||
export const Text = {
|
||||
color: {
|
||||
primary: Color.gray60,
|
||||
secondary: Color.gray50,
|
||||
tertiary: Color.gray40,
|
||||
quarternary: Color.gray30,
|
||||
Inverted: Color.white,
|
||||
},
|
||||
lineHeight: {
|
||||
lg: 1.5,
|
||||
md: 1.2,
|
||||
},
|
||||
|
||||
iconSizeMedium: 16,
|
||||
iconSizeSmall: 14,
|
||||
|
||||
iconStrikeLight: 1.6,
|
||||
iconStrikeMedium: 2,
|
||||
iconStrikeBold: 2.5,
|
||||
};
|
||||
18
packages/twenty-website/src/app/ui/theme/theme.ts
Normal file
18
packages/twenty-website/src/app/ui/theme/theme.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Background } from '@/app/ui/theme/background';
|
||||
import { Border } from '@/app/ui/theme/border';
|
||||
import { Color } from '@/app/ui/theme/colors';
|
||||
import { Font } from '@/app/ui/theme/font';
|
||||
import { Icon } from '@/app/ui/theme/icon';
|
||||
import { Text } from '@/app/ui/theme/text';
|
||||
|
||||
export const Theme = {
|
||||
color: Color,
|
||||
border: Border,
|
||||
background: Background,
|
||||
text: Text,
|
||||
spacingMultiplicator: 4,
|
||||
icon: Icon,
|
||||
font: Font,
|
||||
spacing: (...args: number[]) =>
|
||||
args.map((multiplicator) => `${multiplicator * 4}px`).join(' '),
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
export enum DeviceType {
|
||||
DESKTOP = 'DESKTOP',
|
||||
TABLET = 'TABLET',
|
||||
MOBILE = 'MOBILE',
|
||||
}
|
||||
|
||||
export const useDeviceType = () => {
|
||||
const isTablet = useMediaQuery({
|
||||
query: '(max-width: 1199px) and (min-width: 810px)',
|
||||
});
|
||||
const isMobile = useMediaQuery({ query: '(max-width: 809px)' });
|
||||
|
||||
if (isMobile) {
|
||||
return DeviceType.MOBILE;
|
||||
}
|
||||
if (isTablet) {
|
||||
return DeviceType.TABLET;
|
||||
}
|
||||
return DeviceType.DESKTOP;
|
||||
};
|
||||
@ -1,121 +0,0 @@
|
||||
import { FunctionComponent } from 'react';
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
import { Directory, FileContent, getPosts } from '@/app/get-posts';
|
||||
|
||||
function loadIcon(iconName?: string) {
|
||||
const name = iconName ? iconName : 'IconCategory';
|
||||
|
||||
try {
|
||||
const icon = TablerIcons[
|
||||
name as keyof typeof TablerIcons
|
||||
] as FunctionComponent;
|
||||
return icon as TablerIcons.Icon;
|
||||
} catch (error) {
|
||||
console.error('Icon not found:', iconName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DirectoryItem = ({
|
||||
name,
|
||||
item,
|
||||
}: {
|
||||
name: string;
|
||||
item: Directory | FileContent;
|
||||
}) => {
|
||||
if ('content' in item) {
|
||||
// If the item is a file, we render a link.
|
||||
const Icon = loadIcon(item.itemInfo.icon);
|
||||
|
||||
return (
|
||||
<div key={name}>
|
||||
<Link
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: '#333',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
href={
|
||||
item.itemInfo.path != 'user-guide/home'
|
||||
? `/user-guide/${item.itemInfo.path}`
|
||||
: '/user-guide'
|
||||
}
|
||||
>
|
||||
{Icon ? <Icon size={12} /> : ''}
|
||||
{item.itemInfo.title}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// If the item is a directory, we render the title and the items in the directory.
|
||||
return (
|
||||
<div key={name}>
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
{item.itemInfo.title}
|
||||
</h4>
|
||||
{Object.entries(item).map(([childName, childItem]) => {
|
||||
if (childName !== 'itemInfo') {
|
||||
return (
|
||||
<DirectoryItem
|
||||
key={childName}
|
||||
name={childName}
|
||||
item={childItem as Directory | FileContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default async function UserGuideHome({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
const posts = await getPosts(basePath);
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
borderRight: '1px solid rgba(20, 20, 20, 0.08)',
|
||||
paddingRight: '24px',
|
||||
minWidth: '200px',
|
||||
paddingTop: '48px',
|
||||
}}
|
||||
>
|
||||
{posts['home.mdx'] && (
|
||||
<DirectoryItem
|
||||
name="home"
|
||||
item={posts['home.mdx'] as FileContent}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(posts).map(([name, item]) => {
|
||||
if (name !== 'itemInfo' && name != 'home.mdx') {
|
||||
return (
|
||||
<DirectoryItem
|
||||
key={name}
|
||||
name={name}
|
||||
item={item as Directory | FileContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div style={{ paddingLeft: '24px', paddingRight: '200px' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { getPost } from '@/app/get-posts';
|
||||
|
||||
export default async function UserGuideHome({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string[] };
|
||||
}) {
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
const mainPost = await getPost(
|
||||
params.slug && params.slug.length ? params.slug : ['home'],
|
||||
basePath,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{mainPost?.itemInfo.title}</h2>
|
||||
<div>{mainPost?.content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
packages/twenty-website/src/app/user-guide/[slug]/page.tsx
Normal file
16
packages/twenty-website/src/app/user-guide/[slug]/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import UserGuideContent from '@/app/components/user-guide/UserGuideContent';
|
||||
import { getPost } from '@/app/get-posts';
|
||||
|
||||
export default async function UserGuideSlug({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
}) {
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
const mainPost = await getPost(
|
||||
params.slug && params.slug.length ? params.slug : 'home',
|
||||
basePath,
|
||||
);
|
||||
return mainPost && <UserGuideContent item={mainPost} />;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
export type UserGuideHomeCardsType = {
|
||||
url: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export const UserGuideHomeCards: 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-a-workspace',
|
||||
title: 'Create a Workspace',
|
||||
subtitle: 'Custom objects store unique info in workspaces.',
|
||||
image: '/images/user-guide/home/create-a-workspace.png',
|
||||
},
|
||||
{
|
||||
url: 'import-your-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: 'custom-objects',
|
||||
title: 'Custom Objects',
|
||||
subtitle: 'Custom objects store unique info in workspaces.',
|
||||
image: '/images/user-guide/home/custom-objects.png',
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,29 @@
|
||||
export type IndexSubtopic = {
|
||||
title: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type IndexHeading = {
|
||||
[heading: string]: IndexSubtopic[];
|
||||
};
|
||||
|
||||
export const UserGuideIndex = {
|
||||
'Getting Started': [
|
||||
{ title: 'What is Twenty', url: 'what-is-twenty' },
|
||||
{ title: 'Create a Workspace', url: 'create-a-workspace' },
|
||||
{ title: 'Import your data', url: 'import-your-data' },
|
||||
],
|
||||
Objects: [
|
||||
{ title: 'People', url: 'people' },
|
||||
{ title: 'Companies', url: 'companies' },
|
||||
{ title: 'Opportunities', url: 'opportunities' },
|
||||
{ title: 'Custom Objects', url: 'custom-objects' },
|
||||
{ title: 'Remote Objects', url: 'remote-objects' },
|
||||
],
|
||||
Functions: [
|
||||
{ title: 'Email', url: 'email' },
|
||||
{ title: 'Calendar', url: 'calendar' },
|
||||
{ title: 'Notes', url: 'notes' },
|
||||
{ title: 'Tasks', url: 'tasks' },
|
||||
],
|
||||
};
|
||||
40
packages/twenty-website/src/app/user-guide/layout.tsx
Normal file
40
packages/twenty-website/src/app/user-guide/layout.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import UserGuideSidebar from '@/app/components/user-guide/UserGuideSidebar';
|
||||
import UserGuideTableContents from '@/app/components/user-guide/UserGuideTableContents';
|
||||
import { Theme } from '@/app/ui/theme/theme';
|
||||
import { DeviceType, useDeviceType } from '@/app/ui/utilities/useDeviceType';
|
||||
|
||||
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`
|
||||
width: 20%;
|
||||
`;
|
||||
|
||||
export default function UserGuideLayout({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const deviceType = useDeviceType();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{deviceType !== DeviceType.MOBILE && <UserGuideSidebar />}
|
||||
{children}
|
||||
{deviceType !== DeviceType.DESKTOP ? (
|
||||
<></>
|
||||
) : pathname === '/user-guide' ? (
|
||||
<StyledEmptySideBar />
|
||||
) : (
|
||||
<UserGuideTableContents />
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
5
packages/twenty-website/src/app/user-guide/page.tsx
Normal file
5
packages/twenty-website/src/app/user-guide/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import UserGuideMain from '@/app/components/user-guide/UserGuideMain';
|
||||
|
||||
export default async function UserGuideHome() {
|
||||
return <UserGuideMain />;
|
||||
}
|
||||
Reference in New Issue
Block a user