Marketing website improvements (#3169)

* Website improvement

* Improve website design

* Start writing script for user guide

* Begin adding user guide
This commit is contained in:
Félix Malfait
2023-12-27 16:14:42 +01:00
committed by GitHub
parent c08d8ef838
commit 3d5a364e29
57 changed files with 478 additions and 105 deletions

View File

@ -1,16 +1,21 @@
This is a [Next.js](https://nextjs.org/) project.
# Twenty-Website
This used for the marketing website (twenty.com).
This is not related in anyway to the main app, which you can find in twenty-front and twenty-server.
## Getting Started
First, run the development server:
We're using Nest.JS
From the root directory:
```bash
yarn dev
nx run twenty-website:dev
```
Then open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Or to build in prod:
```bash
nx run twenty-website:build
nx run twenty-website:start
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

View File

@ -1,8 +0,0 @@
import {NextRequest, NextResponse} from "next/server";
export async function GET (request: NextRequest){
const response = await fetch('https://api.github.com/repos/twentyhq/twenty/releases');
const data = await response.json();
return NextResponse.json(data);
}

View File

@ -1,4 +0,0 @@
export default async function BlogPost({ params }: { params: { slug: string } }) {
const posts = {};
return <>Blog Post: {params.slug}</>;
}

View File

@ -1,6 +0,0 @@
export default async function BlogHome() {
const posts = {};
return <>Blog Home</>;
}

View File

@ -1,11 +1,18 @@
'use client'
import styled from '@emotion/styled'
const Container = styled.div`
display: flex;
flex-direction: column;
width: 600px;
@media(max-width: 809px) {
width: 100%;
}`;
export const ContentContainer = ({children}: {children?: React.ReactNode}) => {
return (
<div style={{
width: '600px',
display: 'flex',
flexDirection: 'column',
}}>{children}</div>
<Container>{children}</Container>
)
}

View File

@ -2,7 +2,7 @@
import styled from '@emotion/styled'
import { Logo } from './Logo';
import { DiscordIcon, GithubIcon, LinkedInIcon, XIcon } from "./Icons";
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from "./Icons";
const FooterContainer = styled.div`
@ -11,6 +11,9 @@ const FooterContainer = styled.div`
flex-direction: column;
color: rgb(129, 129, 129);
gap: 32px;
@media(max-width: 809px) {
display: none;
}
`;
const LeftSideFooter = styled.div`
@ -21,7 +24,9 @@ const LeftSideFooter = styled.div`
const RightSideFooter = styled.div`
display: flex;
justify-content: space-between;`;
justify-content: space-between;
gap: 48px;
height: 146px;`;
const RightSideFooterColumn = styled.div`
width: 160px;
@ -46,7 +51,7 @@ const RightSideFooterColumnTitle = styled.div`
export const FooterNav = () => {
export const FooterDesktop = () => {
return <FooterContainer>
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent:'space-between'}}>
<LeftSideFooter>
@ -58,8 +63,8 @@ export const FooterNav = () => {
<RightSideFooter>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Company</RightSideFooterColumnTitle>
<RightSideFooterLink href='/'>Pricing</RightSideFooterLink>
<RightSideFooterLink href='/'>Story</RightSideFooterLink>
<RightSideFooterLink href='/pricing'>Pricing</RightSideFooterLink>
<RightSideFooterLink href='/story'>Story</RightSideFooterLink>
</RightSideFooterColumn>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Resources</RightSideFooterColumnTitle>
@ -91,7 +96,7 @@ export const FooterNav = () => {
<XIcon size='M'/>
</a>
<a href="https://github.com/twentyhq/twenty" target="_blank">
<GithubIcon size='M'/>
<GithubIcon2 size='M'/>
</a>
<a href="https://www.linkedin.com/company/twenty" target="_blank">
<LinkedInIcon size='M'/>

View File

@ -22,14 +22,18 @@ const Nav = styled.nav`
overflow: visible;
padding: 12px 16px 12px 16px;
position: relative;
border-color: rgba(20, 20, 20, 0.08);
transform-origin: 50% 50% 0px;
border-bottom: 1px solid var(--Borders-Light, #F1F1F1);
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
@media(max-width: 809px) {
display: none;
}
`;
const LinkList = styled.div`
display:flex;
flex-direction: row;
gap: 2px;
`;
const ListItem = styled.a`
@ -78,8 +82,7 @@ const StyledButton = styled.div`
const CallToActionContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
gap: 8px;
a {
text-decoration: none;
}
@ -89,6 +92,7 @@ const LinkNextToCTA = styled.a`
display: flex;
align-items: center;
color: rgb(71, 71, 71);
padding: 0px 16px 0px 16px;
span {
text-decoration: underline;
}`;
@ -112,7 +116,7 @@ const ExternalArrow = () => {
}
export const HeaderNav = () => {
export const HeaderDesktop = () => {
const isTwentyDev = false;
@ -124,8 +128,8 @@ export const HeaderNav = () => {
<LinkList>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="/story">Story</ListItem>
<ListItem href="http://docs.twenty.com">Docs <ExternalArrow /></ListItem>
<ListItem href="http://docs.twenty.com"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
<ListItem href="https://docs.twenty.com">Docs <ExternalArrow /></ListItem>
<ListItem href="https://github.com/twentyhq/twenty"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
</LinkList>
<CallToAction />
</Nav>;

View File

@ -0,0 +1,176 @@
'use client'
import styled from '@emotion/styled'
import { Logo } from './Logo';
import { IBM_Plex_Mono } from 'next/font/google';
import { GithubIcon } from './Icons';
const IBMPlexMono = IBM_Plex_Mono({
weight: '500',
subsets: ['latin'],
display: 'swap',
});
const Nav = styled.nav`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
overflow: visible;
padding: 0 12px;
position: relative;
transform-origin: 50% 50% 0px;
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
height: 64px;
width: 100%;
@media(min-width: 810px) {
display: none;
}
`;
const LinkList = styled.div`
display:flex;
flex-direction: column;
`;
const ListItem = styled.a`
color: rgb(71, 71, 71);
text-decoration: none;
display: flex;
gap: 4px;
align-items: center;
border-radius: 8px;
height: 40px;
padding-left: 16px;
padding-right: 16px;
&:hover {
background-color: #F1F1F1;
}
`;
const LogoContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
width:202px;`;
const LogoAddon = styled.div`
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 150%;
`;
const StyledButton = styled.div`
display: flex;
height: 40px;
padding-left: 16px;
padding-right: 16px;
align-items: center;
background-color: #000;
color: #fff;
border-radius: 8px;
font-weight: 500;
border: none;
outline: inherit;
cursor: pointer;
`;
const CallToActionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
a {
text-decoration: none;
}
`;
const LinkNextToCTA = styled.a`
display: flex;
align-items: center;
color: rgb(71, 71, 71);
padding: 0px 16px 0px 16px;
span {
text-decoration: underline;
}`;
const CallToAction = () => {
return <CallToActionContainer>
<LinkNextToCTA href="https://github.com/twentyhq/twenty">Sign in</LinkNextToCTA>
<a href="#">
<StyledButton>
Get Started
</StyledButton>
</a>
</CallToActionContainer>;
};
const ExternalArrow = () => {
return <div style={{width:'14px', height:'14px', fill: 'rgb(179, 179, 179)'}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color="rgb(179, 179, 179)"><g color="rgb(179, 179, 179)" ><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"></path></g></svg>
</div>
}
const HamburgerContainer = styled.div`
height: 44px;
width: 44px;
cursor: pointer;
position: relative;
input {
height: 44px;
width: 44px;
opacity: 0;
}`;
const HamburgerLine1 = styled.div`
height: 2px;
left: calc(50.00000000000002% - 20px / 2);
position: absolute;
top: calc(37.50000000000002% - 2px / 2);
width: 20px;
border-radius: 10px;
background-color: rgb(179, 179, 179);`;
const HamburgerLine2 = styled.div`
height: 2px;
left: calc(50.00000000000002% - 20px / 2);
position: absolute;
top: calc(62.50000000000002% - 2px / 2);
width: 20px;
border-radius: 10px;
background-color: rgb(179, 179, 179);`;
const NavOpen = styled.div`
display:none;`;
export const HeaderMobile = () => {
const isTwentyDev = false;
return <Nav>
<LogoContainer>
<Logo />
{isTwentyDev && <LogoAddon className={IBMPlexMono.className}>for Developers</LogoAddon>}
</LogoContainer>
<HamburgerContainer>
<input type="checkbox" />
<HamburgerLine1 />
<HamburgerLine2 />
</HamburgerContainer>
<NavOpen>
<LinkList>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="/story">Story</ListItem>
<ListItem href="https://docs.twenty.com">Docs <ExternalArrow /></ListItem>
<ListItem href="https://github.com/twentyhq/twenty"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
</LinkList>
<CallToAction />
</NavOpen>
</Nav>;
};

View File

@ -1,4 +1,4 @@
const getSize = size => {
const getSize = (size: string) => {
switch(size) {
case 'S':
return '14px';
@ -36,6 +36,16 @@ export const DiscordIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
export const XIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
let dimension = getSize(size);
return <div style={{width: dimension, height: dimension}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color={color} ><g color={color}><path d="M104,140a12,12,0,1,1-12-12A12,12,0,0,1,104,140Zm60-12a12,12,0,1,0,12,12A12,12,0,0,0,164,128Zm74.45,64.9-67,29.71a16.17,16.17,0,0,1-21.71-9.1l-8.11-22q-6.72.45-13.63.46t-13.63-.46l-8.11,22a16.18,16.18,0,0,1-21.71,9.1l-67-29.71a15.93,15.93,0,0,1-9.06-18.51L38,58A16.07,16.07,0,0,1,51,46.14l36.06-5.93a16.22,16.22,0,0,1,18.26,11.88l3.26,12.84Q118.11,64,128,64t19.4.93l3.26-12.84a16.21,16.21,0,0,1,18.26-11.88L205,46.14A16.07,16.07,0,0,1,218,58l29.53,116.38A15.93,15.93,0,0,1,238.45,192.9ZM232,178.28,202.47,62s0,0-.08,0L166.33,56a.17.17,0,0,0-.17,0l-2.83,11.14c5,.94,10,2.06,14.83,3.42A8,8,0,0,1,176,86.31a8.09,8.09,0,0,1-2.16-.3A172.25,172.25,0,0,0,128,80a172.25,172.25,0,0,0-45.84,6,8,8,0,1,1-4.32-15.4c4.82-1.36,9.78-2.48,14.82-3.42L89.83,56s0,0-.12,0h0L53.61,61.93a.17.17,0,0,0-.09,0L24,178.33,91,208a.23.23,0,0,0,.22,0L98,189.72a173.2,173.2,0,0,1-20.14-4.32A8,8,0,0,1,82.16,170,171.85,171.85,0,0,0,128,176a171.85,171.85,0,0,0,45.84-6,8,8,0,0,1,4.32,15.41A173.2,173.2,0,0,1,158,189.72L164.75,208a.22.22,0,0,0,.21,0Z" fill={color}></path></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg2382164700">
<path d="M 15.418 19.037 L 3.44 3.637 C 3.311 3.471 3.288 3.247 3.381 3.058 C 3.473 2.87 3.665 2.75 3.875 2.75 L 6.148 2.75 C 6.318 2.75 6.478 2.829 6.582 2.963 L 18.56 18.363 C 18.689 18.529 18.712 18.753 18.619 18.942 C 18.527 19.13 18.335 19.25 18.125 19.25 L 15.852 19.25 C 15.682 19.25 15.522 19.171 15.418 19.037 Z" fill="transparent" strokeWidth="1.38" strokeMiterlimit="10" stroke={color}></path>
<path d="M 18.333 2.75 L 3.667 19.25" fill="transparent" strokeWidth="1.38" strokeLinecap="round" strokeMiterlimit="10" stroke={color}></path>
</svg>
</div>
}
}
export const GithubIcon2 = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
let dimension = getSize(size);
return <div style={{width: dimension, height: dimension}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color={color}><g color={color}><path d="M208.31,75.68A59.78,59.78,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58.14,58.14,0,0,0,208.31,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48A8,8,0,0,0,80,73.83a43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06A8,8,0,0,0,119.82,64h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06A43.81,43.81,0,0,1,192,73.83a8.09,8.09,0,0,0,1,7.65A41.72,41.72,0,0,1,200,104Z" fill={color}></path></g></svg>
</div>
}

View File

@ -0,0 +1,7 @@
import Image from 'next/image'
export const PostImage = ({ sources, style }: { sources: { light: string, dark: string }, style?: React.CSSProperties }) => {
return <Image src={sources.light} style={style} alt={sources.light} />
}

View File

@ -0,0 +1,26 @@
*, *::before, *::after {
box-sizing: border-box;
font-smooth: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
box-sizing: border-box;
padding: 0;
margin: 0;
word-wrap: break-word;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
}
a {
color: rgb(129, 129, 129);
&:hover {
color: #000;
}
}

View File

@ -2,19 +2,22 @@ import type { Metadata } from 'next'
import { Gabarito } from 'next/font/google'
import EmotionRootStyleRegistry from './emotion-root-style-registry'
import styled from '@emotion/styled'
import { HeaderNav } from './components/HeaderNav'
import { FooterNav } from './components/FooterNav'
import { HeaderDesktop } from './components/HeaderDesktop'
import { FooterDesktop } from './components/FooterDesktop'
import { HeaderMobile } from '@/app/components/HeaderMobile'
import './layout.css'
export const metadata: Metadata = {
title: 'Twenty.dev',
description: 'Twenty for Developer',
icons: '/favicon.ico',
icons: '/images/core/logo.svg',
}
const gabarito = Gabarito({
weight: ['400', '500'],
subsets: ['latin'],
display: 'swap',
adjustFontFallback: false
})
@ -25,17 +28,14 @@ export default function RootLayout({
}) {
return (
<html lang="en" className={gabarito.className}>
<body style={{
margin: 0,
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale"
}}>
<body>
<EmotionRootStyleRegistry>
<HeaderNav />
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
<HeaderDesktop />
<div className="container">
<HeaderMobile />
{children}
</div>
<FooterNav />
<FooterDesktop />
</EmotionRootStyleRegistry>
</body>
</html>

View File

@ -1,22 +1,25 @@
import { GetStaticProps } from 'next'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { compileMDX } from 'next-mdx-remote/rsc'
import gfm from 'remark-gfm';
import { ContentContainer } from '../components/ContentContainer';
import { visit } from 'unist-util-visit';
import remarkBehead from 'remark-behead';
import type { Metadata } from 'next'
interface Release {
id: number;
name: string;
body: string;
}
}
export const metadata: Metadata= {
title: 'Twenty - Releases',
description: 'Latest releases of Twenty',
}
const Home = async () => {
const res = await fetch(`${process.env.BASE_URL}/api/github`);
const data: Release[] = await res.json();
const response = await fetch('https://api.github.com/repos/twentyhq/twenty/releases');
const data: Release[] = await response.json();
const releases = await Promise.all(
data.map(async (release) => {
@ -51,11 +54,11 @@ interface Release {
<ContentContainer>
<h1>Releases</h1>
{releases.map(release => (
{releases.map((release, index) => (
<div key={release.id}
style={{
padding: '24px 0px 24px 0px',
borderBottom: '1px solid #ccc',
borderBottom: index === releases.length - 1 ? 'none' : '1px solid #ccc',
}}>
<h2>{release.name}</h2>
<div>{release.body}</div>

View File

@ -0,0 +1,11 @@
import { getPost } from "@/app/user-guide/get-posts";
export default async function BlogPost({ params }: { params: { slug: string[] } }) {
const post = await getPost(params.slug as string[]);
console.log(post);
return <div>
<h1>{post?.itemInfo.title}</h1>
<div>{post?.content}</div>
</div>;
}

View File

@ -0,0 +1,97 @@
import fs from 'fs';
import path from 'path';
import { compileMDX } from 'next-mdx-remote/rsc';
import { ReactElement } from 'react';
interface ItemInfo {
title: string;
position?: number;
path: string;
type: 'file' | 'directory';
}
export interface FileContent {
content: ReactElement;
itemInfo: ItemInfo;
}
export interface Directory {
[key: string]: FileContent | Directory | ItemInfo;
itemInfo: ItemInfo;
}
const basePath = '/src/content/user-guide';
async function getFiles(filePath: string, position: number = 0): Promise<Directory> {
const entries = fs.readdirSync(filePath, { withFileTypes: true });
const urlpath = path.toString().split(basePath);
const pathName = urlpath.length > 1 ? urlpath[1] : path.basename(filePath);
console.log(pathName);
const directory: Directory = {
itemInfo: {
title: path.basename(filePath),
position,
type: 'directory',
path: pathName,
},
};
for (const entry of entries) {
if (entry.isDirectory()) {
directory[entry.name] = await getFiles(path.join(filePath, entry.name), position++);
} else if (entry.isFile() && path.extname(entry.name) === '.mdx') {
const fileContent = fs.readFileSync(path.join(filePath, entry.name), 'utf8');
const { content, frontmatter } = await compileMDX<{ title: string, position?: number }>({ source: fileContent, options: { parseFrontmatter: true } });
directory[entry.name] = { content, itemInfo: {...frontmatter, type: 'file', path: pathName + "/" + entry.name.replace(/\.mdx$/, '')} };
}
}
return directory;
}
async function parseFrontMatterAndCategory(directory: Directory, dirPath: string): Promise<Directory> {
const parsedDirectory: Directory = {
itemInfo: directory.itemInfo,
};
for (const entry in directory) {
if (entry !== 'itemInfo' && directory[entry] instanceof Object) {
parsedDirectory[entry] = await parseFrontMatterAndCategory(directory[entry] as Directory, path.join(dirPath, entry));
}
}
const categoryPath = path.join(dirPath, '_category_.json');
if (fs.existsSync(categoryPath)) {
const categoryJson: ItemInfo = JSON.parse(fs.readFileSync(categoryPath, 'utf8'));
parsedDirectory.itemInfo = categoryJson;
}
return parsedDirectory;
}
export async function getPosts(): Promise<Directory> {
const postsDirectory = path.join(process.cwd(), basePath);
const directory = await getFiles(postsDirectory);
return parseFrontMatterAndCategory(directory, postsDirectory);
}
export async function getPost(slug: string[]): Promise<FileContent | null> {
const postsDirectory = path.join(process.cwd(), basePath);
const modifiedSlug = slug.join('/');
const filePath = path.join(postsDirectory, `${modifiedSlug}.mdx`);
console.log(filePath);
if (!fs.existsSync(filePath)) {
return null;
}
const fileContent = fs.readFileSync(filePath, 'utf8');
const { content, frontmatter } = await compileMDX<{ title: string, position?: number }>({ source: fileContent, options: { parseFrontmatter: true } });
return { content, itemInfo: {...frontmatter, type: 'file', path: modifiedSlug }};
}

View File

@ -0,0 +1,47 @@
import { ContentContainer } from '@/app/components/ContentContainer';
import { getPosts, Directory, FileContent } from '@/app/user-guide/get-posts';
import Link from 'next/link';
const DirectoryItem = ({ name, item }: { name: string, item: Directory | FileContent }) => {
if ('content' in item) {
// If the item is a file, we render a link.
return (
<div key={name}>
<Link href={`/user-guide/${item.itemInfo.path}`}>
{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}>
<h2>{item.itemInfo.title}</h2>
{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 BlogHome() {
const posts = await getPosts();
return <ContentContainer>
<h1>User Guide</h1>
<div>
{Object.entries(posts).map(([name, item]) => {
if (name !== 'itemInfo') {
return <DirectoryItem key={name} name={name} item={item as Directory | FileContent} />;
}
})}
</div>
</ContentContainer>;
}

View File

@ -0,0 +1,4 @@
{
"label": "User Guide",
"position": 3
}

View File

@ -0,0 +1,4 @@
{
"title": "Basics",
"position": 1
}

View File

@ -0,0 +1,60 @@
---
title: Get started
displayed_sidebar: userSidebar
sidebar_class_name: hidden
sidebar_position: 0
sidebar_custom_props:
icon: TbUsers
isSidebarRoot: true
---
# Welcome to Twenty's User Guide
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.

View File

@ -0,0 +1,42 @@
---
title: Custom Objects
sidebar_position: 1
sidebar_custom_props:
icon: TbAugmentedReality
---
Objects are structures that allow you to store data (records, attributes, and values) specific to an organization. Twenty provides both standard and custom objects.
Standard objects are in-built objects with a set of attributes available for all users. All workspaces come with three standard objects by default: People, Companies, and Opportunities. Standard objects have standard fields that are also available for all Twenty users, like Account Owner and URL.
Custom objects are objects that you can create to store information that is unique to your organization. They are not built-in; members of your workspace can create and customize custom objects to hold information that standard objects aren't suitable for. For example, if you're Airbnb, you may want to create a custom object for Listings or Reservations.
## Creating a new custom object
To create a new custom object:
1. Go to Settings in the sidebar on the left.
2. Under Workspace, go to Data model. Here you'll be able to see an overview of all your existing Standard and Custom objects (both active and disabled).
<br/>
<img src="/images/user-guide/view-all-objects-light.png" style={{width:'100%', maxWidth:'800px'}}/>
<br/>
3. Click on `+ New object` at the top, then choose Custom as the object type. Enter the name (both singular and plural), choose an icon, and add a description for your custom object and hit Save (at the top right). Using Listing as an example of custom object, the singular would be "listing" and the plural would be "listings" along with a description like "Listings that hosts created to showcase their property."
<br/>
<div style={{textAlign: 'center'}}>
<img src="/images/user-guide/create-custom-object.gif" alt="Create custom object" />
</div>
<br/>
4. Once you create your custom object, you'll be able to manage it. You can edit the name, icon and description, view the different fields, and add more fields.
<div style={{textAlign: 'center'}}>
<img src="/images/user-guide/manage-custom-object.png" alt="Create custom object" />
</div>

View File

@ -0,0 +1,30 @@
---
title: Notes
sidebar_position: 1
sidebar_custom_props:
icon: TbNote
---
import PostImage from '@theme/PostImage';
Easily create a note to keep track of important information.
## Create Notes
To attach a note to the record, go to the <b>Notes</b> tab of a record page and click on `+ New Note`. You can also format, comment, and upload images to your notes.
<img src="images/user-guide/create-new-note-light.png" style={{width:'100%', maxWidth:'800px'}}/>
## Format Notes
You can format your notes right from the editor. Use markdown syntax, press the `/` key or click on the `+` icon on the editor to see the different block options, such as headings, tables, and lists. You can also attach images to your note.
Highlight the text to see more formatting options like bold, italics, and alignment options.
You can also change the background color and text color of each block to highlight important things in your note. To do so, hover over the block you want to format and click on the `⋮⋮` icon besides the `+` icon. Click on <b>Colors</b> to open up all color options for both the text and the background.
## Delete Notes
To delete a note, open it and click on the trashcan icon on the upper right of the screen. This will permanently delete your note.

View File

@ -0,0 +1,50 @@
---
title: Opportunities
sidebar_position: 1
sidebar_custom_props:
icon: TbTargetArrow
---
import PostImage from '@theme/PostImage';
All opportunities are presented in a Kanban board, where each column represents the stage of your workflow and each card represents a record. For each card, you can list the amount, close date, probability, and the point of contact. You can also move each card between stages as it goes through your workflow.
<img src="/images/user-guide/all-opportunities-light.png" style={{width:'100%', maxWidth:'800px'}}/>
## Add and delete stages
You can add as many stages as you'd like to perfectly capture your entire workflow. To add a new stage, click on <b>Options</b> on the top right, choose <b>Stages</b>, and then click on `+ Add stage`. Name your stage and hit `Enter` to create it. Click on the name of the stage and then on on edit to change the stage's name and color.
To delete a stage, click on the stage name or on the `⋮` icon that appears when you hover over a stage. Click on edit to open the menu and click on <b>Delete</b> at the bottom to permanently delete a stage.
## Filter, update, and sort views
It's easy to add filters and update your view to focus on only the most important things. To add a filter:
- Click <b>Filter</b> on the top right of the opportunities board and select the field you want to filter by (Amount, Close date, Company, Creation Date, Person, Pipeline Step, and Point of Contact).
- Select the filter's condition (is/is not, greater than/lesser than) and then choose the value of the field you want to filter by.
- You can either create a new view or update your current view with the filter in place.
- You can also add another filter by following the same steps or clicking on the `+ Add filter` button on top of the columns.
- To remove a filter condition, simply click on the <b>X</b> next to the attribute you used to filter the records.
<img src="/images/user-guide/filter-opportunities-light.png" style={{width:'100%', maxWidth:'800px'}}/>
<br/>
You can also sort your records by Close date, Creation date, and Probability. To do so:
- Click on <b>Sort</b> on the top right.
- Choose Ascending or Descending, and then the field you want to sort the records by.
- You can then update your view or create a new one with the sort filter in place.
## Display fields
You can configure your kanban board to display some fields and hide others. By default, the Amount, Close date, Probability, and Point of Contact are all visible. To hide a field, click on <b>Options</b> on the top right, then on <b>Fields</b> to bring up the list of options. Hover over the field you want to hide to bring up the `-` button. Click on it to hide the field.
You can also rearrange the order of fields by holding down the field name and dragging it to where you want it.
<img src="/images/user-guide/display-fields-light.png" style={{width:'100%', maxWidth:'800px'}}/>
<br/>
You can also hide all the fields, and get an overview of all the opportunities at a glance. To do so, click on <b>Options</b> on the top right and turn on the toggle in front of the <b>Compact view</b> option.
<img src="/images/user-guide/compact-opportunities-view-light.png"style={{width:'100%', maxWidth:'800px'}}/>

View File

@ -0,0 +1,18 @@
---
title: Tasks
sidebar_position: 1
sidebar_custom_props:
icon: TbChecklist
---
import PostImage from '../../../components/PostImage'
You can find all the tasks from across your workspace in the <b>Tasks</b> window in your sidebar. You can also find a dedicated tab for Tasks on each record so you can add and edit tasks directly from each record. Alternatively, you can click on the `+` button on the top right of each record page and then click on <b>Task</b> to create a new task.
<img src="/images/user-guide/create-new-task-light.png" style={{width:'100%', maxWidth:'800px'}}/>
## Tasks page
Switch between upcoming and completed tasks to get an overview of what's pending and what's already been done.
You can also see the tasks assigned to others by changing the assignee from the top right of the Tasks page, and edit each task to update the content, due dates, and assignee. You can also comment on each task.

View File

@ -0,0 +1,4 @@
{
"title": "Integrations",
"position": 2
}

View File

@ -0,0 +1,27 @@
---
title: Connect Twenty and Zapier
sidebar_position: 3
sidebar_custom_props:
icon: TbBrandZapier
---
:::caution
Twenty integration is currently being registered in the public Zapier repository, you may not find it until the publishing process is complete.
:::
Sync Twenty with 3000+ apps using [Zapier](https://zapier.com/), and automate your work. Here's how you can connect Twenty to Zapier:
1. Go to Zapier and log in.
2. Click on `+ Create Zap` in the left sidebar.
3. Choose the application you want to set as the trigger. A trigger refers to an event that starts the automation.
4. Select Twenty as the action. An action is the event performed whenever an application triggers an automation. [Learn more about triggers and actions in Zapier.](https://zapier.com/how-it-works)
5. Once you choose the Twenty account that you want to use for your automation, you'll have to authorize it by adding an API key. You can learn [how to generate your API key here.](user-guide/integrations/generating-api-keys.mdx)
6. Enter your API key and click on 'Yes, Continue to Twenty.'
<div style={{textAlign: 'center'}}>
<img src="/images/user-guide/connect-zapier.png" alt="Connect Twenty to Zapier" />
</div>
You can now continue creating your automation!

View File

@ -0,0 +1,26 @@
---
title: Generating an API Key
sidebar_position: 2
sidebar_custom_props:
icon: TbApi
---
To generate an API key:
1. Go to Settings in the sidebar on the left.
2. Under Workspace, go to Developers. Here, you'll see a list of active keys that you or your team have created.
3. To generate a new key, click on `+ Create key` on the top right.
4. Give your API key a name, an expiration date, and a logo.
5. Hit save to see your API key.
6. Since the key is only visible once, make sure you store it somewhere safe.
:::caution Note
Since your API key contains sensitive information, you shouldn't share it with services you don't fully trust. If leaked, someone can use it maliciously. If you think your API key is no longer secure, make sure you disable it and generate a new one.
:::
## Regenerating API key
To regenerate an API key, click on the key you want to regenerate. You'll then be able to see a button to regenerate the API key.

View File

@ -0,0 +1,4 @@
{
"title": "Others",
"position": 3
}

View File

@ -0,0 +1,23 @@
---
title: Glossary
sidebar_position: 3
sidebar_custom_props:
icon: TbVocabulary
---
### Company & People
The CRM has two fundamental types of records:
- A `Company` represents a business or organization.
- `People` represent your company's current and prospective customers or clients.
### Pipelines
A `Pipeline` is a way to track a business process. Pipelines are present within a *module* and have *stages*:
- A **module** contains the logic for a certain business process (for example: sales, recruiting).
- **Stages** map the steps in your process (for example: new, ongoing, won, lost).
### Views
With views, you can customize how your records are displayed. You can have different filters and sort settings for each view.
### Workspace
A `Workspace` typically represents a company using Twenty. It holds all the records and data that you and your team members add to Twenty.
It has a single domain name, which is typically the domain name your company uses for employee email addresses.

View File

@ -0,0 +1,39 @@
---
title: Tips
sidebar_position: 1
sidebar_custom_props:
icon: TbInfoCircle
---
## Update workspace name & logo
Workspace admins can edit its name and logo in settings.
- From the sidebar, go to <b>Settings</b>.
- Under <b>Workspace</b>, go to <b>General</b>.
- Edit the name and logo. Your changes will be saved automatically.
## Enable dark mode
Not a fan of light mode? Easily switch to dark mode with these steps:
- From the sidebar, go to <b>Settings</b>.
- Under <b>User</b>, go to <b>Appearance</b>.
- Select <b>Dark</b>. Your changes will be saved automatically.
## Account settings
Configure your user account and set your preferences.
- From the sidebar, go to <b>Settings</b>.
- Under <b>User</b>, go to <b>Profile</b> to edit your name and profile picture. You can upload PNGs, GIFs, and JPEGs.
- Manage your accounts and configure your email and calendar settings in <b>Accounts</b>.
- Your changes will be saved automatically.
## Invite & manage members
Admins can easily invite new members any time.
- From the sidebar, go to <b>Settings</b>.
- Under <b>Workspace</b>, go to <b>Members</b>.
- Use the invite link to add more members to your workspace or simply delete existing ones.