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:
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -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 />;
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
title: Get started
|
||||
position: 0
|
||||
icon: IconUsers
|
||||
info: This is homepage
|
||||
---
|
||||
|
||||
The purpose of this user guide is to help you learn how you can use Twenty to build the CRM you want.
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
---
|
||||
title: What is Twenty
|
||||
position: 0
|
||||
icon: IconUsers
|
||||
info: A brief on Twenty's commitment to reshaping CRM with Open Source
|
||||
image: /images/user-guide/home/what-is-twenty.png
|
||||
---
|
||||
|
||||
## What is Twenty
|
||||
|
||||
The purpose of this user guide is to help you learn how you can use Twenty to build the CRM you want.
|
||||
|
||||
## Quick Search
|
||||
|
||||
You'll see a search bar at the top of your sidebar. You can also bring up the command bar with the `cmd`/`ctrl` + `k` shortcut to navigate through your workspace, and find people, companies, notes, and more.
|
||||
|
||||
The command bar also supports other shortcuts for navigation.
|
||||
|
||||
## Create Pre-filtered Views
|
||||
|
||||
Twenty allows you to add filters to see data that meets certain criteria and hides the rest. You can apply multiple filters at once.
|
||||
|
||||
To create a filter in your workspace, click Filter at the top right and select the attribute you'd like to filter your records by. Create your filter and then save changes in a new view by clicking `+ Create view`.
|
||||
|
||||
The filtered view is now available to your whole team.
|
||||
|
||||
## Add Stages For Opportunities
|
||||
|
||||
You can also add more stages to the opportunities board.
|
||||
|
||||
On the <b>Opportunities</b> page, click on <b>Options</b>, <b>Stages</b>, then `+ Add Stage`.
|
||||
|
||||
You can also edit the stage by clicking on the name.
|
||||
|
||||
## Create Notes and Tasks For Each Record
|
||||
|
||||
You can attach notes and tasks to each record. With Notes, you can keep a record of any observations, comments, and interactions, and keep track of all items that require action with Tasks.
|
||||
|
||||
There are multiple ways to create notes and tasks. Learn more about [Notes](./basics/notes.mdx) and [Tasks](./basics/tasks.mdx).
|
||||
|
||||
## Upload Files For Each Record
|
||||
|
||||
You can also upload and attach files for each record. To do so, expand a record, and head over to the <b>Files</b> tab. You'll then see the `+ Add file` button.
|
||||
|
||||
<img src="/images/user-guide/attach-files-to-records-light.png" style={{width:'100%', maxWidth:'800px'}}/>
|
||||
|
||||
## Add Records To Favorites
|
||||
|
||||
You can add records to your favorites for quick access. To do so, expand the record you want to add, and click on the heart icon on the top right. You'll now be able to see your favorite records in your sidebar right above your workspace.
|
||||
|
||||
<img src="/images/user-guide/view-favorite-records-light.png" style={{width:'100%', maxWidth:'800px'}}/>
|
||||
|
||||
## Import data
|
||||
|
||||
You can easily import People and Companies data into Twenty from other apps using a .csv, .xslx, or .xsl file. In the <b>Companies</b> or <b>People</b> page, click on <b>Options</b> and then on <b>Import</b>.
|
||||
|
||||
Upload your file, match the columns, and validate the data to import it.
|
||||
Reference in New Issue
Block a user