Marketing website improvements (#3169)
* Website improvement * Improve website design * Start writing script for user guide * Begin adding user guide
@ -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
|
||||
```
|
||||
|
After Width: | Height: | Size: 898 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 865 KiB |
|
After Width: | Height: | Size: 987 KiB |
|
After Width: | Height: | Size: 861 KiB |
|
After Width: | Height: | Size: 932 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 416 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 863 KiB |
|
After Width: | Height: | Size: 995 KiB |
@ -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);
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export default async function BlogPost({ params }: { params: { slug: string } }) {
|
||||
const posts = {};
|
||||
return <>Blog Post: {params.slug}</>;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
|
||||
|
||||
export default async function BlogHome() {
|
||||
const posts = {};
|
||||
return <>Blog Home</>;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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'/>
|
||||
@ -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>;
|
||||
176
packages/twenty-website/src/app/components/HeaderMobile.tsx
Normal 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>;
|
||||
};
|
||||
|
||||
@ -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>
|
||||
}
|
||||
7
packages/twenty-website/src/app/components/PostImage.tsx
Normal 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} />
|
||||
}
|
||||
26
packages/twenty-website/src/app/layout.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
97
packages/twenty-website/src/app/user-guide/get-posts.tsx
Normal 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 }};
|
||||
}
|
||||
47
packages/twenty-website/src/app/user-guide/page.tsx
Normal 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>;
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "User Guide",
|
||||
"position": 3
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Basics",
|
||||
"position": 1
|
||||
}
|
||||
@ -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.
|
||||
@ -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>
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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'}}/>
|
||||
@ -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.
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Integrations",
|
||||
"position": 2
|
||||
}
|
||||
@ -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!
|
||||
@ -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.
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Others",
|
||||
"position": 3
|
||||
}
|
||||
@ -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.
|
||||
@ -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.
|
||||