Marketing improvements 3 (#3175)

* Improve marketing website

* User guide with icons

* Add TOC

* Linter

* Basic GraphQL playground

* Very basic contributors page

* Failed attempt to integrate REST playground

* Yarn

* Begin contributors DB

* Improve contributors page
This commit is contained in:
Félix Malfait
2023-12-29 11:17:32 +01:00
committed by GitHub
parent fa8a04743c
commit c422045ea6
46 changed files with 7589 additions and 687 deletions

View File

@ -0,0 +1,62 @@
'use client'
import styled from '@emotion/styled';
export interface User {
login: string;
avatarUrl: string;
}
const AvatarGridContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
grid-gap: 10px;
`;
const AvatarItem = styled.div`
position: relative;
width: 100%;
img {
width: 100%;
height: auto;
display: block;
}
.username {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.7);
color: white;
text-align: center;
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease, visibility 0.3s;
}
&:hover .username {
visibility: visible;
opacity: 1;
}
`;
import React from 'react';
const AvatarGrid = ({ users }: { users: User[] }) => {
return (
<AvatarGridContainer>
{users.map(user => (
<AvatarItem key={user.login}>
<img src={user.avatarUrl} alt={user.login} />
<span className="username">{user.login}</span>
</AvatarItem>
))}
</AvatarGridContainer>
);
};
export default AvatarGrid;

View File

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

View File

@ -0,0 +1,16 @@
export 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>
);
};

View File

@ -1,111 +1,154 @@
'use client'
'use client';
import styled from '@emotion/styled'
import styled from '@emotion/styled';
import { Logo } from './Logo';
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from "./Icons";
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from './Icons';
import { usePathname } from 'next/navigation';
const FooterContainer = styled.div`
padding: 64px 96px 64px 96px;
display: flex;
flex-direction: column;
color: rgb(129, 129, 129);
gap: 32px;
@media(max-width: 809px) {
display: none;
}
padding: 64px 96px 64px 96px;
display: flex;
flex-direction: column;
color: rgb(129, 129, 129);
gap: 32px;
@media (max-width: 809px) {
display: none;
}
`;
const LeftSideFooter = styled.div`
width: 36Opx;
display: flex;
flex-direction: column;
gap: 16px;`;
width: 36Opx;
display: flex;
flex-direction: column;
gap: 16px;
`;
const RightSideFooter = styled.div`
display: flex;
justify-content: space-between;
gap: 48px;
height: 146px;`;
display: flex;
justify-content: space-between;
gap: 48px;
height: 146px;
`;
const RightSideFooterColumn = styled.div`
width: 160px;
display: flex;
flex-direction: column;
gap: 8px;
width: 160px;
display: flex;
flex-direction: column;
gap: 8px;
`;
const RightSideFooterLink = styled.a`
color: rgb(129, 129, 129);
text-decoration: none;
&:hover {
text-decoration: underline;
color: #000;
}`;
color: rgb(129, 129, 129);
text-decoration: none;
&:hover {
text-decoration: underline;
color: #000;
}
`;
const RightSideFooterColumnTitle = styled.div`
font-size: 20px;
font-weight: 500;
color: #000;
`;
font-size: 20px;
font-weight: 500;
color: #000;
`;
export const FooterDesktop = () => {
return <FooterContainer>
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent:'space-between'}}>
<LeftSideFooter>
<Logo />
<div>
The #1 Open Source CRM
</div>
</LeftSideFooter>
<RightSideFooter>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Company</RightSideFooterColumnTitle>
<RightSideFooterLink href='/pricing'>Pricing</RightSideFooterLink>
<RightSideFooterLink href='/story'>Story</RightSideFooterLink>
</RightSideFooterColumn>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Resources</RightSideFooterColumnTitle>
<RightSideFooterLink href='https://docs.twenty.com'>Documentation</RightSideFooterLink>
<RightSideFooterLink href='/releases'>Changelog</RightSideFooterLink>
</RightSideFooterColumn>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Other</RightSideFooterColumnTitle>
<RightSideFooterLink href='/oss-friends'>OSS Friends</RightSideFooterLink>
<RightSideFooterLink href='/legal/terms'>Terms of Service</RightSideFooterLink>
<RightSideFooterLink href='/legal/privacy'>Privacy Policy</RightSideFooterLink>
</RightSideFooterColumn>
</RightSideFooter>
const path = usePathname();
const isTwentyDev = path.includes('developers');
if (isTwentyDev) return;
return (
<FooterContainer>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<LeftSideFooter>
<Logo />
<div>The #1 Open Source CRM</div>
</LeftSideFooter>
<RightSideFooter>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Company</RightSideFooterColumnTitle>
<RightSideFooterLink href="/pricing">Pricing</RightSideFooterLink>
<RightSideFooterLink href="/story">Story</RightSideFooterLink>
</RightSideFooterColumn>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Resources</RightSideFooterColumnTitle>
<RightSideFooterLink href="https://docs.twenty.com">
Documentation
</RightSideFooterLink>
<RightSideFooterLink href="/releases">
Changelog
</RightSideFooterLink>
</RightSideFooterColumn>
<RightSideFooterColumn>
<RightSideFooterColumnTitle>Other</RightSideFooterColumnTitle>
<RightSideFooterLink href="/oss-friends">
OSS Friends
</RightSideFooterLink>
<RightSideFooterLink href="/legal/terms">
Terms of Service
</RightSideFooterLink>
<RightSideFooterLink href="/legal/privacy">
Privacy Policy
</RightSideFooterLink>
</RightSideFooterColumn>
</RightSideFooter>
</div>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
borderTop: '1px solid rgb(179, 179, 179)',
paddingTop: '32px',
}}
>
<div>
<span style={{ fontFamily: 'Inter, sans-serif' }}>©</span>
2023 Twenty PBC
</div>
<div style={{
width: '100%',
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent:'space-between',
borderTop: '1px solid rgb(179, 179, 179)',
paddingTop: '32px'
}}>
<div>
<span style={{fontFamily: "Inter, sans-serif"}}>©</span>
2023 Twenty PBC
</div>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent:'space-between', gap:'10px'}}>
<a href="https://x.com/twentycrm" target="_blank">
<XIcon size='M'/>
</a>
<a href="https://github.com/twentyhq/twenty" target="_blank">
<GithubIcon2 size='M'/>
</a>
<a href="https://www.linkedin.com/company/twenty" target="_blank">
<LinkedInIcon size='M'/>
</a>
<a href="https://discord.gg/UfGNZJfAG6" target="_blank">
<DiscordIcon size='M' />
</a>
</div>
justifyContent: 'space-between',
gap: '10px',
}}
>
<a href="https://x.com/twentycrm" target="_blank" rel="noreferrer">
<XIcon size="M" />
</a>
<a
href="https://github.com/twentyhq/twenty"
target="_blank"
rel="noreferrer"
>
<GithubIcon2 size="M" />
</a>
<a
href="https://www.linkedin.com/company/twenty"
target="_blank"
rel="noreferrer"
>
<LinkedInIcon size="M" />
</a>
<a
href="https://discord.gg/UfGNZJfAG6"
target="_blank"
rel="noreferrer"
>
<DiscordIcon size="M" />
</a>
</div>
</div>
</FooterContainer>
;
}
);
};

View File

@ -1,137 +1,188 @@
'use client'
'use client';
import styled from '@emotion/styled'
import styled from '@emotion/styled';
import { Logo } from './Logo';
import { IBM_Plex_Mono } from 'next/font/google';
import { GithubIcon } from './Icons';
import { DiscordIcon, GithubIcon, GithubIcon2, XIcon } from './Icons';
import { usePathname } from 'next/navigation';
import { ExternalArrow } from '@/app/components/ExternalArrow';
const IBMPlexMono = IBM_Plex_Mono({
weight: '500',
subsets: ['latin'],
display: 'swap',
});
weight: '500',
subsets: ['latin'],
display: 'swap',
});
const Nav = styled.nav`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
overflow: visible;
padding: 12px 16px 12px 16px;
position: relative;
transform-origin: 50% 50% 0px;
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
overflow: visible;
padding: 12px 16px 12px 16px;
position: relative;
transform-origin: 50% 50% 0px;
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
@media(max-width: 809px) {
display: none;
}
@media (max-width: 809px) {
display: none;
}
`;
const LinkList = styled.div`
display:flex;
flex-direction: row;
gap: 2px;
`;
display: flex;
flex-direction: row;
gap: 2px;
`;
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;
}
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;`;
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%;
`;
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;
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;
}
`;
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;
}`;
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 path = usePathname();
const isTwentyDev = path.includes('developers');
return (
<CallToActionContainer>
{isTwentyDev ? (
<>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '10px',
}}
>
<a href="https://x.com/twentycrm" target="_blank" rel="noreferrer">
<XIcon size="M" />
</a>
<a
href="https://github.com/twentyhq/twenty"
target="_blank"
rel="noreferrer"
>
<GithubIcon2 size="M" />
</a>
<a
href="https://discord.gg/UfGNZJfAG6"
target="_blank"
rel="noreferrer"
>
<DiscordIcon size="M" />
</a>
</div>
</>
) : (
<>
<LinkNextToCTA href="https://github.com/twentyhq/twenty">
Sign in
</LinkNextToCTA>
<a href="https://twenty.com/stripe-redirection">
<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>
}
export const HeaderDesktop = () => {
const path = usePathname();
const isTwentyDev = path.includes('developers');
const isTwentyDev = false;
return <Nav>
<LogoContainer>
<Logo />
{isTwentyDev && <LogoAddon className={IBMPlexMono.className}>for Developers</LogoAddon>}
</LogoContainer>
return (
<Nav>
<LogoContainer>
<Logo />
{isTwentyDev && (
<LogoAddon className={IBMPlexMono.className}>
for Developers
</LogoAddon>
)}
</LogoContainer>
{isTwentyDev ? (
<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>
<ListItem href="/developers/docs">Docs</ListItem>
<ListItem href="/developers/contributors">Contributors</ListItem>
<ListItem href="/">
Cloud <ExternalArrow />
</ListItem>
</LinkList>
<CallToAction />
</Nav>;
) : (
<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 />
</Nav>
);
};

View File

@ -1,176 +1,216 @@
'use client'
'use client';
import styled from '@emotion/styled'
import styled from '@emotion/styled';
import { Logo } from './Logo';
import { IBM_Plex_Mono } from 'next/font/google';
import { GithubIcon } from './Icons';
import { useState } from 'react';
import { ExternalArrow } from '@/app/components/ExternalArrow';
const IBMPlexMono = IBM_Plex_Mono({
weight: '500',
subsets: ['latin'],
display: 'swap',
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;
}
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;
`;
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;
}
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;`;
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%;
`;
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;
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;
}
`;
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;
}`;
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>;
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;
cursor: pointer;
position: relative;
input {
height: 44px;
width: 44px;
opacity: 0;
}`;
opacity: 0;
}
#line1 {
transition: transform 0.5s;
transition-timing-function: ease-in-out;
}
#line2 {
transition: transform 0.5s;
transition-timing-function: ease-in-out;
}
#menu-input:checked ~ #line1 {
transform: rotate(45deg) translate(7px);
}
#menu-input:checked ~ #line2 {
transform: rotate(-45deg) translate(7px);
}
`;
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);`;
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);`;
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;`;
flex-direction: column;
align-items: center;
`;
const MobileMenu = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
export const HeaderMobile = () => {
const isTwentyDev = false;
const isTwentyDev = false;
const [menuOpen, setMenuOpen] = useState(false);
return <Nav>
const toggleMenu = () => {
setMenuOpen(!menuOpen);
};
return (
<MobileMenu>
<Nav>
<LogoContainer>
<Logo />
{isTwentyDev && <LogoAddon className={IBMPlexMono.className}>for Developers</LogoAddon>}
<Logo />
{isTwentyDev && (
<LogoAddon className={IBMPlexMono.className}>
for Developers
</LogoAddon>
)}
</LogoContainer>
<HamburgerContainer>
<input type="checkbox" />
<HamburgerLine1 />
<HamburgerLine2 />
<input type="checkbox" id="menu-input" onChange={toggleMenu} />
<HamburgerLine1 id="line1" />
<HamburgerLine2 id="line2" />
</HamburgerContainer>
<NavOpen>
</Nav>
<NavOpen style={{ display: menuOpen ? 'flex' : 'none' }}>
<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>
<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>;
</NavOpen>
</MobileMenu>
);
};

View File

@ -1,51 +1,119 @@
const getSize = (size: string) => {
switch(size) {
case 'S':
return '14px';
case 'M':
return '24px';
case 'L':
return '48px';
default:
return '14px';
}
switch (size) {
case 'S':
return '14px';
case 'M':
return '24px';
case 'L':
return '48px';
default:
return '14px';
}
};
export const GithubIcon = ({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 14 14"><path d="M 6.979 0 C 3.12 0 0 3.143 0 7.031 C 0 10.139 1.999 12.77 4.772 13.701 C 5.119 13.771 5.246 13.55 5.246 13.364 C 5.246 13.201 5.234 12.642 5.234 12.06 C 3.293 12.479 2.889 11.222 2.889 11.222 C 2.577 10.407 2.114 10.197 2.114 10.197 C 1.479 9.767 2.161 9.767 2.161 9.767 C 2.866 9.813 3.235 10.488 3.235 10.488 C 3.859 11.559 4.865 11.257 5.269 11.07 C 5.327 10.616 5.512 10.302 5.708 10.127 C 4.16 9.964 2.531 9.359 2.531 6.658 C 2.531 5.89 2.808 5.262 3.247 4.773 C 3.178 4.598 2.935 3.876 3.316 2.91 C 3.316 2.91 3.906 2.724 5.234 3.632 C 5.803 3.478 6.39 3.4 6.979 3.399 C 7.568 3.399 8.169 3.481 8.724 3.632 C 10.053 2.724 10.642 2.91 10.642 2.91 C 11.023 3.876 10.781 4.598 10.711 4.773 C 11.162 5.262 11.428 5.89 11.428 6.658 C 11.428 9.359 9.799 9.953 8.239 10.127 C 8.493 10.349 8.712 10.768 8.712 11.431 C 8.712 12.374 8.701 13.131 8.701 13.363 C 8.701 13.55 8.828 13.771 9.175 13.701 C 11.948 12.77 13.947 10.139 13.947 7.031 C 13.958 3.143 10.827 0 6.979 0 Z" fill={color}></path></svg>
export const GithubIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
const dimension = getSize(size);
return (
<div style={{ width: dimension, height: dimension }}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14">
<path
d="M 6.979 0 C 3.12 0 0 3.143 0 7.031 C 0 10.139 1.999 12.77 4.772 13.701 C 5.119 13.771 5.246 13.55 5.246 13.364 C 5.246 13.201 5.234 12.642 5.234 12.06 C 3.293 12.479 2.889 11.222 2.889 11.222 C 2.577 10.407 2.114 10.197 2.114 10.197 C 1.479 9.767 2.161 9.767 2.161 9.767 C 2.866 9.813 3.235 10.488 3.235 10.488 C 3.859 11.559 4.865 11.257 5.269 11.07 C 5.327 10.616 5.512 10.302 5.708 10.127 C 4.16 9.964 2.531 9.359 2.531 6.658 C 2.531 5.89 2.808 5.262 3.247 4.773 C 3.178 4.598 2.935 3.876 3.316 2.91 C 3.316 2.91 3.906 2.724 5.234 3.632 C 5.803 3.478 6.39 3.4 6.979 3.399 C 7.568 3.399 8.169 3.481 8.724 3.632 C 10.053 2.724 10.642 2.91 10.642 2.91 C 11.023 3.876 10.781 4.598 10.711 4.773 C 11.162 5.262 11.428 5.89 11.428 6.658 C 11.428 9.359 9.799 9.953 8.239 10.127 C 8.493 10.349 8.712 10.768 8.712 11.431 C 8.712 12.374 8.701 13.131 8.701 13.363 C 8.701 13.55 8.828 13.771 9.175 13.701 C 11.948 12.77 13.947 10.139 13.947 7.031 C 13.958 3.143 10.827 0 6.979 0 Z"
fill={color}
></path>
</svg>
</div>
}
);
};
export const LinkedInIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
let dimension = getSize(size);
export const LinkedInIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
const 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="M216,24H40A16,16,0,0,0,24,40V216a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V40A16,16,0,0,0,216,24Zm0,192H40V40H216V216ZM96,112v64a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0Zm88,28v36a8,8,0,0,1-16,0V140a20,20,0,0,0-40,0v36a8,8,0,0,1-16,0V112a8,8,0,0,1,15.79-1.78A36,36,0,0,1,184,140ZM100,84A12,12,0,1,1,88,72,12,12,0,0,1,100,84Z" fill={color}></path></g></svg>
</div>;
}
export const DiscordIcon = ({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>
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="M216,24H40A16,16,0,0,0,24,40V216a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V40A16,16,0,0,0,216,24Zm0,192H40V40H216V216ZM96,112v64a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0Zm88,28v36a8,8,0,0,1-16,0V140a20,20,0,0,0-40,0v36a8,8,0,0,1-16,0V112a8,8,0,0,1,15.79-1.78A36,36,0,0,1,184,140ZM100,84A12,12,0,1,1,88,72,12,12,0,0,1,100,84Z"
fill={color}
></path>
</g>
</svg>
</div>
}
);
};
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 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>
export const DiscordIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
const 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>
</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>
export const XIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
const dimension = getSize(size);
return (
<div style={{ width: dimension, height: dimension }}>
<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)' }) => {
const 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

@ -1,16 +1,17 @@
import styled from "@emotion/styled";
import styled from '@emotion/styled';
const Link = styled.a`
display:block;
image-rendering: pixelated;
flex-shrink: 0;
background-size: 100% 100%;
border-radius: 8px;
height: 40px;
width: 40px;
background-image: url("images/core/logo.svg");
opacity: 1;`;
display: block;
image-rendering: pixelated;
flex-shrink: 0;
background-size: 100% 100%;
border-radius: 8px;
height: 40px;
width: 40px;
background-image: url('/images/core/logo.svg');
opacity: 1;
`;
export const Logo = () => {
return <Link href="/" />;
return <Link href="/" />;
};

View File

@ -0,0 +1,30 @@
import TokenForm, {
TokenFormProps,
} from '@/app/components/PlaygroundTokenForm';
import React, { ReactNode, useState } from 'react';
type PlaygroundProps = TokenFormProps & {
children?: ReactNode;
};
const Playground = ({
children,
setOpenApiJson,
setToken,
}: PlaygroundProps) => {
const [isTokenValid, setIsTokenValid] = useState(false);
return (
<>
<TokenForm
setOpenApiJson={setOpenApiJson}
setToken={setToken}
isTokenValid={isTokenValid}
setIsTokenValid={setIsTokenValid}
/>
{isTokenValid && children}
</>
);
};
export default Playground;

View File

@ -0,0 +1,190 @@
import React, { useEffect, useState } from 'react';
import { parseJson } from 'nx/src/utils/json';
import { TbLoader2 } from 'react-icons/tb';
import styled from '@emotion/styled';
export type TokenFormProps = {
setOpenApiJson?: (json: object) => void;
setToken?: (token: string) => void;
isTokenValid: boolean;
setIsTokenValid: (boolean: boolean) => void;
};
const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
`;
const Form = styled.form`
text-align: center;
padding: 50px;
`;
const StyledLink = styled.a`
color: #16233f;
text-decoration: none;
position: relative;
font-weight: bold;
transition: color 0.3s ease;
[data-theme='dark'] & {
color: #a3c0f8;
}
`;
const Input = styled.input`
padding: 6px;
margin: 20px 0 5px 0;
max-width: 460px;
width: 100%;
box-sizing: border-box;
background-color: #f3f3f3;
border: 1px solid #ddd;
border-radius: 4px;
[data-theme='dark'] & {
background-color: #16233f;
}
&.invalid {
border: 1px solid #f83e3e;
}
`;
const TokenInvalid = styled.span`
color: #f83e3e;
font-size: 12px;
`;
const Loader = styled(TbLoader2)`
color: #16233f;
font-size: 2rem;
animation: animate 2s infinite;
[data-theme='dark'] & {
color: #a3c0f8;
}
&.not-visible {
visibility: hidden;
}
@keyframes animate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(720deg);
}
}
`;
const LoaderContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 50px;
`;
const TokenForm = ({
setOpenApiJson,
setToken,
isTokenValid,
setIsTokenValid,
}: TokenFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const token =
parseJson(localStorage.getItem('TryIt_securitySchemeValues') || '')
?.bearerAuth ?? '';
const updateToken = async (event: React.ChangeEvent<HTMLInputElement>) => {
localStorage.setItem(
'TryIt_securitySchemeValues',
JSON.stringify({ bearerAuth: event.target.value }),
);
await submitToken(event.target.value);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const validateToken = (openApiJson: any) =>
setIsTokenValid(!!openApiJson.tags);
const getJson = async (token: string) => {
setIsLoading(true);
return await fetch('https://api.twenty.com/open-api', {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => res.json())
.then((result) => {
validateToken(result);
setIsLoading(false);
return result;
})
.catch(() => setIsLoading(false));
};
const submitToken = async (token: string) => {
if (isLoading) return;
const json = await getJson(token);
setToken && setToken(token);
setOpenApiJson && setOpenApiJson(json);
};
useEffect(() => {
(async () => {
await submitToken(token);
})();
});
// We load playground style using useEffect as it breaks remaining docs style
useEffect(() => {
const styleElement = document.createElement('style');
styleElement.innerHTML = TokenForm.toString();
document.head.append(styleElement);
return () => styleElement.remove();
}, []);
return (
!isTokenValid && (
<Container>
<Form>
<label>
To load your playground schema,{' '}
<StyledLink href="https://app.twenty.com/settings/developers/api-keys">
generate an API key
</StyledLink>{' '}
and paste it here:
</label>
<p>
<Input
className={token && !isLoading ? 'input invalid' : 'input'}
type="text"
readOnly={isLoading}
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMD..."
defaultValue={token}
onChange={updateToken}
/>
<TokenInvalid
className={`${(!token || isLoading) && 'not-visible'}`}
>
Token invalid
</TokenInvalid>
<LoaderContainer>
<Loader className={`${!isLoading && 'not-visible'}`} />
</LoaderContainer>
</p>
</Form>
</Container>
)
);
};
export default TokenForm;

View File

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