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

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