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:
Kanav Arora
2024-02-23 21:09:48 +05:30
committed by GitHub
parent 35a2178cde
commit 4b22c0404e
33 changed files with 914 additions and 161 deletions

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

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 },
};
}

View File

@ -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;
}

View File

@ -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 />

View File

@ -0,0 +1,7 @@
export type { TablerIconsProps } from '@tabler/icons-react';
export {
IconBook,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
} from '@tabler/icons-react';

View 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),
},
};

View 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,
};

View 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})`;
};

View 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',
};

View 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,
},
};

View 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,
};

View 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(' '),
};

View File

@ -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;
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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} />;
}

View File

@ -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',
},
];

View File

@ -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' },
],
};

View 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>
);
}

View File

@ -0,0 +1,5 @@
import UserGuideMain from '@/app/components/user-guide/UserGuideMain';
export default async function UserGuideHome() {
return <UserGuideMain />;
}

View File

@ -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.

View File

@ -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.