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:
62
packages/twenty-website/src/app/components/AvatarGrid.tsx
Normal file
62
packages/twenty-website/src/app/components/AvatarGrid.tsx
Normal 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;
|
||||
@ -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>;
|
||||
};
|
||||
|
||||
16
packages/twenty-website/src/app/components/ExternalArrow.tsx
Normal file
16
packages/twenty-website/src/app/components/ExternalArrow.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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="/" />;
|
||||
};
|
||||
|
||||
30
packages/twenty-website/src/app/components/Playground.tsx
Normal file
30
packages/twenty-website/src/app/components/Playground.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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} />;
|
||||
};
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { slug: string } }) {
|
||||
const db = new Database('db.sqlite', { readonly: true });
|
||||
|
||||
if(params.slug !== 'users' && params.slug !== 'labels' && params.slug !== 'pullRequests') {
|
||||
return Response.json({ error: 'Invalid table name' }, { status: 400 });
|
||||
}
|
||||
|
||||
const rows = db.prepare('SELECT * FROM ' + params.slug).all();
|
||||
|
||||
db.close();
|
||||
|
||||
return Response.json(rows);
|
||||
}
|
||||
@ -0,0 +1,288 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
const db = new Database('db.sqlite', { verbose: console.log });
|
||||
|
||||
interface LabelNode {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AuthorNode {
|
||||
resourcePath: string;
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PullRequestNode {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
closedAt: string;
|
||||
mergedAt: string;
|
||||
author: AuthorNode;
|
||||
labels: {
|
||||
nodes: LabelNode[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IssueNode {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
closedAt: string;
|
||||
author: AuthorNode;
|
||||
labels: {
|
||||
nodes: LabelNode[];
|
||||
};
|
||||
}
|
||||
|
||||
interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface PullRequests {
|
||||
nodes: PullRequestNode[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
interface Issues {
|
||||
nodes: IssueNode[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
interface AssignableUserNode {
|
||||
login: string;
|
||||
}
|
||||
|
||||
interface AssignableUsers {
|
||||
nodes: AssignableUserNode[];
|
||||
}
|
||||
|
||||
interface RepoData {
|
||||
repository: {
|
||||
pullRequests: PullRequests;
|
||||
issues: Issues;
|
||||
assignableUsers: AssignableUsers;
|
||||
};
|
||||
}
|
||||
|
||||
const query = graphql.defaults({
|
||||
headers: {
|
||||
Authorization: 'bearer ' + process.env.GITHUB_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
async function fetchData(cursor: string | null = null, isIssues: boolean = false, accumulatedData: Array<PullRequestNode | IssueNode> = []): Promise<Array<PullRequestNode | IssueNode>> {
|
||||
const { repository } = await query<RepoData>(`
|
||||
query ($cursor: String) {
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${isIssues}) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
closedAt
|
||||
mergedAt
|
||||
author {
|
||||
resourcePath
|
||||
login
|
||||
avatarUrl(size: 460)
|
||||
url
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
issues(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${!isIssues}) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
body
|
||||
createdAt
|
||||
updatedAt
|
||||
closedAt
|
||||
author {
|
||||
resourcePath
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, { cursor });
|
||||
|
||||
const newAccumulatedData: Array<PullRequestNode | IssueNode> = [...accumulatedData, ...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes)];
|
||||
const pageInfo = isIssues ? repository.issues.pageInfo : repository.pullRequests.pageInfo;
|
||||
|
||||
if (pageInfo.hasNextPage) {
|
||||
return fetchData(pageInfo.endCursor, isIssues, newAccumulatedData);
|
||||
} else {
|
||||
return newAccumulatedData;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAssignableUsers(): Promise<Set<string>> {
|
||||
const { repository } = await query<RepoData>(`
|
||||
query {
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
assignableUsers(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return new Set(repository.assignableUsers.nodes.map(user => user.login));
|
||||
}
|
||||
|
||||
const initDb = () => {
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS pullRequests (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
body TEXT,
|
||||
createdAt TEXT,
|
||||
updatedAt TEXT,
|
||||
closedAt TEXT,
|
||||
mergedAt TEXT,
|
||||
authorId TEXT,
|
||||
FOREIGN KEY (authorId) REFERENCES users(id)
|
||||
);
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS issues (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
body TEXT,
|
||||
createdAt TEXT,
|
||||
updatedAt TEXT,
|
||||
closedAt TEXT,
|
||||
authorId TEXT,
|
||||
FOREIGN KEY (authorId) REFERENCES users(id)
|
||||
);
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
login TEXT,
|
||||
avatarUrl TEXT,
|
||||
url TEXT,
|
||||
isEmployee BOOLEAN
|
||||
);
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS labels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
color TEXT,
|
||||
description TEXT
|
||||
);
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS pullRequestLabels (
|
||||
pullRequestId TEXT,
|
||||
labelId TEXT,
|
||||
FOREIGN KEY (pullRequestId) REFERENCES pullRequests(id),
|
||||
FOREIGN KEY (labelId) REFERENCES labels(id)
|
||||
);
|
||||
`).run();
|
||||
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS issueLabels (
|
||||
issueId TEXT,
|
||||
labelId TEXT,
|
||||
FOREIGN KEY (issueId) REFERENCES issues(id),
|
||||
FOREIGN KEY (labelId) REFERENCES labels(id)
|
||||
);
|
||||
`).run();
|
||||
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
|
||||
initDb();
|
||||
|
||||
// TODO if we ever hit API Rate Limiting
|
||||
const lastPRCursor = null;
|
||||
const lastIssueCursor = null;
|
||||
|
||||
const assignableUsers = await fetchAssignableUsers();
|
||||
const prs = await fetchData(lastPRCursor) as Array<PullRequestNode>;
|
||||
const issues = await fetchData(lastIssueCursor) as Array<IssueNode>;
|
||||
|
||||
const insertPR = db.prepare('INSERT INTO pullRequests (id, title, body, createdAt, updatedAt, closedAt, mergedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertIssue = db.prepare('INSERT INTO issues (id, title, body, createdAt, updatedAt, closedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertUser = db.prepare('INSERT INTO users (id, login, avatarUrl, url, isEmployee) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertLabel = db.prepare('INSERT INTO labels (id, name, color, description) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
|
||||
const insertPullRequestLabel = db.prepare('INSERT INTO pullRequestLabels (pullRequestId, labelId) VALUES (?, ?)');
|
||||
const insertIssueLabel = db.prepare('INSERT INTO issueLabels (issueId, labelId) VALUES (?, ?)');
|
||||
|
||||
for (const pr of prs) {
|
||||
console.log(pr);
|
||||
if(pr.author == null) { continue; }
|
||||
insertUser.run(pr.author.resourcePath, pr.author.login, pr.author.avatarUrl, pr.author.url, assignableUsers.has(pr.author.login) ? 1 : 0);
|
||||
insertPR.run(pr.id, pr.title, pr.body, pr.createdAt, pr.updatedAt, pr.closedAt, pr.mergedAt, pr.author.resourcePath);
|
||||
|
||||
for (const label of pr.labels.nodes) {
|
||||
insertLabel.run(label.id, label.name, label.color, label.description);
|
||||
insertPullRequestLabel.run(pr.id, label.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
if(issue.author == null) { continue; }
|
||||
insertUser.run(issue.author.resourcePath, issue.author.login, issue.author.avatarUrl, issue.author.url, assignableUsers.has(issue.author.login) ? 1 : 0);
|
||||
|
||||
insertIssue.run(issue.id, issue.title, issue.body, issue.createdAt, issue.updatedAt, issue.closedAt, issue.author.resourcePath);
|
||||
|
||||
for (const label of issue.labels.nodes) {
|
||||
insertLabel.run(label.id, label.name, label.color, label.description);
|
||||
insertIssueLabel.run(issue.id, label.id);
|
||||
}
|
||||
}
|
||||
|
||||
db.close();
|
||||
|
||||
return new Response("Data synced", { status: 200 });
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import Image from 'next/image';
|
||||
import Database from 'better-sqlite3';
|
||||
import AvatarGrid from '@/app/components/AvatarGrid';
|
||||
|
||||
interface Contributor {
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
pullRequestCount: number;
|
||||
}
|
||||
|
||||
const Contributors = async () => {
|
||||
|
||||
|
||||
const db = new Database('db.sqlite', { readonly: true });
|
||||
|
||||
const contributors = db.prepare(`SELECT
|
||||
u.login,
|
||||
u.avatarUrl,
|
||||
COUNT(pr.id) AS pullRequestCount
|
||||
FROM
|
||||
users u
|
||||
JOIN
|
||||
pullRequests pr ON u.id = pr.authorId
|
||||
GROUP BY
|
||||
u.id
|
||||
ORDER BY
|
||||
pullRequestCount DESC;
|
||||
`).all() as Contributor[];
|
||||
|
||||
db.close();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Top Contributors</h1>
|
||||
<AvatarGrid users={contributors} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contributors;
|
||||
@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import dynamic from 'next/dynamic';
|
||||
import 'graphiql/graphiql.css';
|
||||
|
||||
// Create a named function for your component
|
||||
function GraphiQLComponent() {
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: 'https://api.twenty.com/graphql',
|
||||
});
|
||||
|
||||
return <GraphiQL fetcher={fetcher} />;
|
||||
}
|
||||
|
||||
// Dynamically import the GraphiQL component with SSR disabled
|
||||
const GraphiQLWithNoSSR = dynamic(() => Promise.resolve(GraphiQLComponent), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const GraphQLDocs = () => {
|
||||
return <GraphiQLWithNoSSR />;
|
||||
};
|
||||
|
||||
export default GraphQLDocs;
|
||||
63
packages/twenty-website/src/app/developers/docs/layout.tsx
Normal file
63
packages/twenty-website/src/app/developers/docs/layout.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
|
||||
const DeveloperDocsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ContentContainer>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
borderRight: '1px solid rgba(20, 20, 20, 0.08)',
|
||||
paddingRight: '24px',
|
||||
minWidth: '200px',
|
||||
paddingTop: '48px',
|
||||
}}
|
||||
>
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
Install & Maintain
|
||||
</h4>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Local setup
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Self-hosting
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Upgrade guide
|
||||
</a>{' '}
|
||||
<br /> <br />
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
Resources
|
||||
</h4>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Contributors Guide
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a
|
||||
style={{ textDecoration: 'none', color: '#333' }}
|
||||
href="/developers/docs/graphql"
|
||||
>
|
||||
GraphQL API
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a
|
||||
style={{ textDecoration: 'none', color: '#333', display: 'flex' }}
|
||||
href="/developers/rest"
|
||||
>
|
||||
Rest API
|
||||
</a>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Twenty UI
|
||||
</a>{' '}
|
||||
<br />
|
||||
</div>
|
||||
<div style={{ padding: '24px', minHeight: '80vh', width: '100%' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperDocsLayout;
|
||||
9
packages/twenty-website/src/app/developers/docs/page.tsx
Normal file
9
packages/twenty-website/src/app/developers/docs/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const DeveloperDocs = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Developer Docs</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperDocs;
|
||||
@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
/*import { API } from '@stoplight/elements';/
|
||||
|
||||
import '@stoplight/elements/styles.min.css';
|
||||
|
||||
/*
|
||||
const RestApiComponent = ({ openApiJson }: { openApiJson: any }) => {
|
||||
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
// styleElement.innerHTML = spotlightTheme.toString();
|
||||
document.head.append(styleElement);
|
||||
|
||||
return () => styleElement.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<API apiDescriptionDocument={JSON.stringify(openApiJson)} router="hash" />
|
||||
);
|
||||
};*/
|
||||
|
||||
const RestApi = () => {
|
||||
/* const [openApiJson, setOpenApiJson] = useState({});
|
||||
|
||||
const children = <RestApiComponent openApiJson={openApiJson} />;*/
|
||||
|
||||
return <>API</>;
|
||||
|
||||
// return <Playground setOpenApiJson={setOpenApiJson}>{children}</Playground>;
|
||||
};
|
||||
|
||||
export default RestApi;
|
||||
9
packages/twenty-website/src/app/developers/page.tsx
Normal file
9
packages/twenty-website/src/app/developers/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const Developers = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This page should probably be built on Framer</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Developers;
|
||||
@ -1,37 +1,37 @@
|
||||
'use client'
|
||||
'use client';
|
||||
|
||||
import { CacheProvider } from '@emotion/react'
|
||||
import createCache from '@emotion/cache'
|
||||
import { useServerInsertedHTML } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { useServerInsertedHTML } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function RootStyleRegistry({ children }) {
|
||||
const [{ cache, flush }] = useState(() => {
|
||||
const cache = createCache({ key: 'emotion-cache' })
|
||||
cache.compat = true
|
||||
const prevInsert = cache.insert
|
||||
let inserted = []
|
||||
const cache = createCache({ key: 'emotion-cache' });
|
||||
cache.compat = true;
|
||||
const prevInsert = cache.insert;
|
||||
let inserted = [];
|
||||
cache.insert = (...args) => {
|
||||
const serialized = args[1]
|
||||
const serialized = args[1];
|
||||
if (cache.inserted[serialized.name] === undefined) {
|
||||
inserted.push(serialized.name)
|
||||
inserted.push(serialized.name);
|
||||
}
|
||||
return prevInsert(...args)
|
||||
}
|
||||
return prevInsert(...args);
|
||||
};
|
||||
const flush = () => {
|
||||
const prevInserted = inserted
|
||||
inserted = []
|
||||
return prevInserted
|
||||
}
|
||||
return { cache, flush }
|
||||
})
|
||||
const prevInserted = inserted;
|
||||
inserted = [];
|
||||
return prevInserted;
|
||||
};
|
||||
return { cache, flush };
|
||||
});
|
||||
|
||||
useServerInsertedHTML(() => {
|
||||
const names = flush()
|
||||
if (names.length === 0) return null
|
||||
let styles = ''
|
||||
const names = flush();
|
||||
if (names.length === 0) return null;
|
||||
let styles = '';
|
||||
for (const name of names) {
|
||||
styles += cache.inserted[name]
|
||||
styles += cache.inserted[name];
|
||||
}
|
||||
return (
|
||||
<style
|
||||
@ -40,8 +40,8 @@ export default function RootStyleRegistry({ children }) {
|
||||
__html: styles,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return <CacheProvider value={cache}>{children}</CacheProvider>
|
||||
return <CacheProvider value={cache}>{children}</CacheProvider>;
|
||||
}
|
||||
|
||||
@ -2,12 +2,16 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import { ReactElement } from 'react';
|
||||
import gfm from 'remark-gfm';
|
||||
import rehypeToc from '@jsdevtools/rehype-toc';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
|
||||
interface ItemInfo {
|
||||
title: string;
|
||||
position?: number;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
@ -20,15 +24,15 @@ export interface Directory {
|
||||
itemInfo: ItemInfo;
|
||||
}
|
||||
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
|
||||
async function getFiles(filePath: string, position: number = 0): Promise<Directory> {
|
||||
async function getFiles(
|
||||
filePath: string,
|
||||
basePath: 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: {
|
||||
@ -41,57 +45,97 @@ async function getFiles(filePath: string, position: number = 0): Promise<Directo
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
directory[entry.name] = await getFiles(path.join(filePath, entry.name), position++);
|
||||
directory[entry.name] = await getFiles(
|
||||
path.join(filePath, entry.name),
|
||||
basePath,
|
||||
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$/, '')} };
|
||||
const { content, frontmatter } = await compileMDXFile(
|
||||
path.join(filePath, entry.name),
|
||||
);
|
||||
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> {
|
||||
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));
|
||||
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'));
|
||||
const categoryJson: ItemInfo = JSON.parse(
|
||||
fs.readFileSync(categoryPath, 'utf8'),
|
||||
);
|
||||
parsedDirectory.itemInfo = categoryJson;
|
||||
}
|
||||
|
||||
return parsedDirectory;
|
||||
}
|
||||
|
||||
export async function getPosts(): Promise<Directory> {
|
||||
export async function compileMDXFile(filePath: string, addToc: boolean = true) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const compiled = await compileMDX<{ title: string; position?: number }>({
|
||||
source: fileContent,
|
||||
options: {
|
||||
parseFrontmatter: true,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [rehypeSlug, ...(addToc ? [rehypeToc] : [])],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
export async function getPosts(basePath: string): Promise<Directory> {
|
||||
const postsDirectory = path.join(process.cwd(), basePath);
|
||||
const directory = await getFiles(postsDirectory);
|
||||
const directory = await getFiles(postsDirectory, basePath);
|
||||
return parseFrontMatterAndCategory(directory, postsDirectory);
|
||||
}
|
||||
|
||||
export async function getPost(slug: string[]): Promise<FileContent | null> {
|
||||
export async function getPost(
|
||||
slug: string[],
|
||||
basePath: string,
|
||||
): Promise<FileContent | null> {
|
||||
const postsDirectory = path.join(process.cwd(), basePath);
|
||||
const modifiedSlug = slug.join('/');
|
||||
const filePath = path.join(postsDirectory, `${modifiedSlug}.mdx`);
|
||||
|
||||
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 }};
|
||||
const { content, frontmatter } = await compileMDXFile(filePath);
|
||||
|
||||
return {
|
||||
content,
|
||||
itemInfo: { ...frontmatter, type: 'file', path: modifiedSlug },
|
||||
};
|
||||
}
|
||||
@ -18,9 +18,20 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: rgb(94, 30, 4);
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(129, 129, 129);
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
nav.toc {
|
||||
width: 200px;
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 0;
|
||||
}
|
||||
@ -1,43 +1,41 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Gabarito } from 'next/font/google'
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry'
|
||||
import styled from '@emotion/styled'
|
||||
import { HeaderDesktop } from './components/HeaderDesktop'
|
||||
import { FooterDesktop } from './components/FooterDesktop'
|
||||
import { HeaderMobile } from '@/app/components/HeaderMobile'
|
||||
import './layout.css'
|
||||
import type { Metadata } from 'next';
|
||||
import { Gabarito } from 'next/font/google';
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry';
|
||||
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: '/images/core/logo.svg',
|
||||
}
|
||||
};
|
||||
|
||||
const gabarito = Gabarito({
|
||||
weight: ['400', '500'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
adjustFontFallback: false
|
||||
})
|
||||
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={gabarito.className}>
|
||||
<body>
|
||||
<EmotionRootStyleRegistry>
|
||||
<HeaderDesktop />
|
||||
<div className="container">
|
||||
<HeaderMobile />
|
||||
{children}
|
||||
</div>
|
||||
<FooterDesktop />
|
||||
<EmotionRootStyleRegistry>
|
||||
<HeaderDesktop />
|
||||
<div className="container">
|
||||
<HeaderMobile />
|
||||
{children}
|
||||
</div>
|
||||
<FooterDesktop />
|
||||
</EmotionRootStyleRegistry>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { ContentContainer } from './components/ContentContainer'
|
||||
import { ContentContainer } from './components/ContentContainer';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<ContentContainer>
|
||||
<div style={{ minHeight: '60vh', marginTop: '50px' }}>
|
||||
Part of the website is built directly with Framer, including the homepage. <br />
|
||||
We use Clouflare to split the traffic between the two sites.
|
||||
Part of the website is built directly with Framer, including the
|
||||
homepage. <br />
|
||||
We use Clouflare to split the traffic between the two sites.
|
||||
</div>
|
||||
</ContentContainer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,71 +1,71 @@
|
||||
import { compileMDX } from 'next-mdx-remote/rsc'
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import gfm from 'remark-gfm';
|
||||
import { ContentContainer } from '../components/ContentContainer';
|
||||
import remarkBehead from 'remark-behead';
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
interface Release {
|
||||
id: number;
|
||||
name: string;
|
||||
body: string;
|
||||
id: number;
|
||||
name: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
|
||||
export const metadata: Metadata= {
|
||||
export const metadata: Metadata = {
|
||||
title: 'Twenty - Releases',
|
||||
description: 'Latest releases of Twenty',
|
||||
}
|
||||
};
|
||||
|
||||
const Home = async () => {
|
||||
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) => {
|
||||
let mdxSource;
|
||||
try {
|
||||
mdxSource = await compileMDX({
|
||||
source: release.body,
|
||||
options: {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
gfm,
|
||||
[remarkBehead, { depth: 2 }],
|
||||
],
|
||||
}
|
||||
},
|
||||
});
|
||||
mdxSource = mdxSource.content;
|
||||
} catch(error) {
|
||||
console.error('An error occurred during MDX rendering:', error);
|
||||
mdxSource = `<p>Oops! Something went wrong.</p> ${error}`;;
|
||||
}
|
||||
const Home = async () => {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/twentyhq/twenty/releases',
|
||||
);
|
||||
const data: Release[] = await response.json();
|
||||
|
||||
return {
|
||||
id: release.id,
|
||||
name: release.name,
|
||||
body: mdxSource,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<h1>Releases</h1>
|
||||
const releases = await Promise.all(
|
||||
data.map(async (release) => {
|
||||
let mdxSource;
|
||||
try {
|
||||
mdxSource = await compileMDX({
|
||||
source: release.body,
|
||||
options: {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm, [remarkBehead, { depth: 2 }]],
|
||||
},
|
||||
},
|
||||
});
|
||||
mdxSource = mdxSource.content;
|
||||
} catch (error) {
|
||||
console.error('An error occurred during MDX rendering:', error);
|
||||
mdxSource = `<p>Oops! Something went wrong.</p> ${error}`;
|
||||
}
|
||||
|
||||
{releases.map((release, index) => (
|
||||
<div key={release.id}
|
||||
return {
|
||||
id: release.id,
|
||||
name: release.name,
|
||||
body: mdxSource,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<h1>Releases</h1>
|
||||
|
||||
{releases.map((release, index) => (
|
||||
<div
|
||||
key={release.id}
|
||||
style={{
|
||||
padding: '24px 0px 24px 0px',
|
||||
borderBottom: index === releases.length - 1 ? 'none' : '1px solid #ccc',
|
||||
}}>
|
||||
<h2>{release.name}</h2>
|
||||
<div>{release.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</ContentContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home;
|
||||
borderBottom:
|
||||
index === releases.length - 1 ? 'none' : '1px solid #ccc',
|
||||
}}
|
||||
>
|
||||
<h2>{release.name}</h2>
|
||||
<div>{release.body}</div>
|
||||
</div>
|
||||
))}
|
||||
</ContentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
import { getPosts, Directory, FileContent } from '@/app/get-posts';
|
||||
import Link from 'next/link';
|
||||
import * as TablerIcons from '@tabler/icons-react';
|
||||
import { FunctionComponent } from 'react';
|
||||
|
||||
function loadIcon(iconName?: string) {
|
||||
const name = iconName ? iconName : 'IconCategory';
|
||||
|
||||
try {
|
||||
const icon = TablerIcons[
|
||||
name as keyof typeof TablerIcons
|
||||
] as FunctionComponent;
|
||||
return icon as TablerIcons.Icon;
|
||||
} catch (error) {
|
||||
console.error('Icon not found:', iconName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DirectoryItem = ({
|
||||
name,
|
||||
item,
|
||||
}: {
|
||||
name: string;
|
||||
item: Directory | FileContent;
|
||||
}) => {
|
||||
if ('content' in item) {
|
||||
// If the item is a file, we render a link.
|
||||
const Icon = loadIcon(item.itemInfo.icon);
|
||||
|
||||
return (
|
||||
<div key={name}>
|
||||
<Link
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: '#333',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
href={
|
||||
item.itemInfo.path != 'user-guide/home'
|
||||
? `/user-guide/${item.itemInfo.path}`
|
||||
: '/user-guide'
|
||||
}
|
||||
>
|
||||
{Icon ? <Icon size={12} /> : ''}
|
||||
{item.itemInfo.title}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// If the item is a directory, we render the title and the items in the directory.
|
||||
return (
|
||||
<div key={name}>
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
{item.itemInfo.title}
|
||||
</h4>
|
||||
{Object.entries(item).map(([childName, childItem]) => {
|
||||
if (childName !== 'itemInfo') {
|
||||
return (
|
||||
<DirectoryItem
|
||||
key={childName}
|
||||
name={childName}
|
||||
item={childItem as Directory | FileContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default async function UserGuideHome({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
const posts = await getPosts(basePath);
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
borderRight: '1px solid rgba(20, 20, 20, 0.08)',
|
||||
paddingRight: '24px',
|
||||
minWidth: '200px',
|
||||
paddingTop: '48px',
|
||||
}}
|
||||
>
|
||||
{posts['home.mdx'] && (
|
||||
<DirectoryItem
|
||||
name="home"
|
||||
item={posts['home.mdx'] as FileContent}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(posts).map(([name, item]) => {
|
||||
if (name !== 'itemInfo' && name != 'home.mdx') {
|
||||
return (
|
||||
<DirectoryItem
|
||||
key={name}
|
||||
name={name}
|
||||
item={item as Directory | FileContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div style={{ paddingLeft: '24px', paddingRight: '200px' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { getPost } from '@/app/get-posts';
|
||||
|
||||
export default async function UserGuideHome({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string[] };
|
||||
}) {
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
const mainPost = await getPost(
|
||||
params.slug && params.slug.length ? params.slug : ['home'],
|
||||
basePath,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{mainPost?.itemInfo.title}</h2>
|
||||
<div>{mainPost?.content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user