Marketing website improvements (#3169)
* Website improvement * Improve website design * Start writing script for user guide * Begin adding user guide
This commit is contained in:
@ -1,8 +0,0 @@
|
||||
import {NextRequest, NextResponse} from "next/server";
|
||||
|
||||
export async function GET (request: NextRequest){
|
||||
const response = await fetch('https://api.github.com/repos/twentyhq/twenty/releases');
|
||||
const data = await response.json();
|
||||
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export default async function BlogPost({ params }: { params: { slug: string } }) {
|
||||
const posts = {};
|
||||
return <>Blog Post: {params.slug}</>;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
|
||||
|
||||
export default async function BlogHome() {
|
||||
const posts = {};
|
||||
return <>Blog Home</>;
|
||||
}
|
||||
@ -1,11 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import styled from '@emotion/styled'
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 600px;
|
||||
@media(max-width: 809px) {
|
||||
width: 100%;
|
||||
}`;
|
||||
|
||||
|
||||
export const ContentContainer = ({children}: {children?: React.ReactNode}) => {
|
||||
return (
|
||||
<div style={{
|
||||
width: '600px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>{children}</div>
|
||||
<Container>{children}</Container>
|
||||
)
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import styled from '@emotion/styled'
|
||||
import { Logo } from './Logo';
|
||||
import { DiscordIcon, GithubIcon, LinkedInIcon, XIcon } from "./Icons";
|
||||
import { DiscordIcon, GithubIcon2, LinkedInIcon, XIcon } from "./Icons";
|
||||
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
@ -11,6 +11,9 @@ const FooterContainer = styled.div`
|
||||
flex-direction: column;
|
||||
color: rgb(129, 129, 129);
|
||||
gap: 32px;
|
||||
@media(max-width: 809px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LeftSideFooter = styled.div`
|
||||
@ -21,7 +24,9 @@ const LeftSideFooter = styled.div`
|
||||
|
||||
const RightSideFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;`;
|
||||
justify-content: space-between;
|
||||
gap: 48px;
|
||||
height: 146px;`;
|
||||
|
||||
const RightSideFooterColumn = styled.div`
|
||||
width: 160px;
|
||||
@ -46,7 +51,7 @@ const RightSideFooterColumnTitle = styled.div`
|
||||
|
||||
|
||||
|
||||
export const FooterNav = () => {
|
||||
export const FooterDesktop = () => {
|
||||
return <FooterContainer>
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent:'space-between'}}>
|
||||
<LeftSideFooter>
|
||||
@ -58,8 +63,8 @@ export const FooterNav = () => {
|
||||
<RightSideFooter>
|
||||
<RightSideFooterColumn>
|
||||
<RightSideFooterColumnTitle>Company</RightSideFooterColumnTitle>
|
||||
<RightSideFooterLink href='/'>Pricing</RightSideFooterLink>
|
||||
<RightSideFooterLink href='/'>Story</RightSideFooterLink>
|
||||
<RightSideFooterLink href='/pricing'>Pricing</RightSideFooterLink>
|
||||
<RightSideFooterLink href='/story'>Story</RightSideFooterLink>
|
||||
</RightSideFooterColumn>
|
||||
<RightSideFooterColumn>
|
||||
<RightSideFooterColumnTitle>Resources</RightSideFooterColumnTitle>
|
||||
@ -91,7 +96,7 @@ export const FooterNav = () => {
|
||||
<XIcon size='M'/>
|
||||
</a>
|
||||
<a href="https://github.com/twentyhq/twenty" target="_blank">
|
||||
<GithubIcon size='M'/>
|
||||
<GithubIcon2 size='M'/>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/twenty" target="_blank">
|
||||
<LinkedInIcon size='M'/>
|
||||
@ -22,14 +22,18 @@ const Nav = styled.nav`
|
||||
overflow: visible;
|
||||
padding: 12px 16px 12px 16px;
|
||||
position: relative;
|
||||
border-color: rgba(20, 20, 20, 0.08);
|
||||
transform-origin: 50% 50% 0px;
|
||||
border-bottom: 1px solid var(--Borders-Light, #F1F1F1);
|
||||
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
|
||||
|
||||
@media(max-width: 809px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkList = styled.div`
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
`;
|
||||
|
||||
const ListItem = styled.a`
|
||||
@ -78,8 +82,7 @@ const StyledButton = styled.div`
|
||||
const CallToActionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
gap: 8px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@ -89,6 +92,7 @@ const LinkNextToCTA = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(71, 71, 71);
|
||||
padding: 0px 16px 0px 16px;
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}`;
|
||||
@ -112,7 +116,7 @@ const ExternalArrow = () => {
|
||||
|
||||
}
|
||||
|
||||
export const HeaderNav = () => {
|
||||
export const HeaderDesktop = () => {
|
||||
|
||||
const isTwentyDev = false;
|
||||
|
||||
@ -124,8 +128,8 @@ export const HeaderNav = () => {
|
||||
<LinkList>
|
||||
<ListItem href="/pricing">Pricing</ListItem>
|
||||
<ListItem href="/story">Story</ListItem>
|
||||
<ListItem href="http://docs.twenty.com">Docs <ExternalArrow /></ListItem>
|
||||
<ListItem href="http://docs.twenty.com"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
|
||||
<ListItem href="https://docs.twenty.com">Docs <ExternalArrow /></ListItem>
|
||||
<ListItem href="https://github.com/twentyhq/twenty"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
|
||||
</LinkList>
|
||||
<CallToAction />
|
||||
</Nav>;
|
||||
176
packages/twenty-website/src/app/components/HeaderMobile.tsx
Normal file
176
packages/twenty-website/src/app/components/HeaderMobile.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
'use client'
|
||||
|
||||
import styled from '@emotion/styled'
|
||||
import { Logo } from './Logo';
|
||||
import { IBM_Plex_Mono } from 'next/font/google';
|
||||
import { GithubIcon } from './Icons';
|
||||
|
||||
const IBMPlexMono = IBM_Plex_Mono({
|
||||
weight: '500',
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
transform-origin: 50% 50% 0px;
|
||||
border-bottom: 1px solid rgba(20, 20, 20, 0.08);
|
||||
height: 64px;
|
||||
width: 100%;
|
||||
@media(min-width: 810px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkList = styled.div`
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const ListItem = styled.a`
|
||||
color: rgb(71, 71, 71);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
&:hover {
|
||||
background-color: #F1F1F1;
|
||||
}
|
||||
`;
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width:202px;`;
|
||||
|
||||
const LogoAddon = styled.div`
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 150%;
|
||||
`;
|
||||
|
||||
const StyledButton = styled.div`
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
align-items: center;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
outline: inherit;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const CallToActionContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkNextToCTA = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(71, 71, 71);
|
||||
padding: 0px 16px 0px 16px;
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}`;
|
||||
|
||||
const CallToAction = () => {
|
||||
return <CallToActionContainer>
|
||||
<LinkNextToCTA href="https://github.com/twentyhq/twenty">Sign in</LinkNextToCTA>
|
||||
<a href="#">
|
||||
<StyledButton>
|
||||
Get Started
|
||||
</StyledButton>
|
||||
</a>
|
||||
</CallToActionContainer>;
|
||||
};
|
||||
|
||||
|
||||
const ExternalArrow = () => {
|
||||
return <div style={{width:'14px', height:'14px', fill: 'rgb(179, 179, 179)'}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color="rgb(179, 179, 179)"><g color="rgb(179, 179, 179)" ><path d="M200,64V168a8,8,0,0,1-16,0V83.31L69.66,197.66a8,8,0,0,1-11.32-11.32L172.69,72H88a8,8,0,0,1,0-16H192A8,8,0,0,1,200,64Z"></path></g></svg>
|
||||
</div>
|
||||
}
|
||||
|
||||
const HamburgerContainer = styled.div`
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
input {
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
opacity: 0;
|
||||
}`;
|
||||
|
||||
const HamburgerLine1 = styled.div`
|
||||
height: 2px;
|
||||
left: calc(50.00000000000002% - 20px / 2);
|
||||
position: absolute;
|
||||
top: calc(37.50000000000002% - 2px / 2);
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(179, 179, 179);`;
|
||||
|
||||
const HamburgerLine2 = styled.div`
|
||||
height: 2px;
|
||||
left: calc(50.00000000000002% - 20px / 2);
|
||||
position: absolute;
|
||||
top: calc(62.50000000000002% - 2px / 2);
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: rgb(179, 179, 179);`;
|
||||
|
||||
const NavOpen = styled.div`
|
||||
display:none;`;
|
||||
|
||||
|
||||
export const HeaderMobile = () => {
|
||||
|
||||
const isTwentyDev = false;
|
||||
|
||||
return <Nav>
|
||||
<LogoContainer>
|
||||
<Logo />
|
||||
{isTwentyDev && <LogoAddon className={IBMPlexMono.className}>for Developers</LogoAddon>}
|
||||
</LogoContainer>
|
||||
<HamburgerContainer>
|
||||
<input type="checkbox" />
|
||||
<HamburgerLine1 />
|
||||
<HamburgerLine2 />
|
||||
</HamburgerContainer>
|
||||
|
||||
<NavOpen>
|
||||
<LinkList>
|
||||
<ListItem href="/pricing">Pricing</ListItem>
|
||||
<ListItem href="/story">Story</ListItem>
|
||||
<ListItem href="https://docs.twenty.com">Docs <ExternalArrow /></ListItem>
|
||||
<ListItem href="https://github.com/twentyhq/twenty"><GithubIcon color='rgb(71,71,71)' /> 5.7k <ExternalArrow /></ListItem>
|
||||
</LinkList>
|
||||
<CallToAction />
|
||||
</NavOpen>
|
||||
</Nav>;
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const getSize = size => {
|
||||
const getSize = (size: string) => {
|
||||
switch(size) {
|
||||
case 'S':
|
||||
return '14px';
|
||||
@ -36,6 +36,16 @@ export const DiscordIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
|
||||
export const XIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
|
||||
let dimension = getSize(size);
|
||||
return <div style={{width: dimension, height: dimension}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color={color} ><g color={color}><path d="M104,140a12,12,0,1,1-12-12A12,12,0,0,1,104,140Zm60-12a12,12,0,1,0,12,12A12,12,0,0,0,164,128Zm74.45,64.9-67,29.71a16.17,16.17,0,0,1-21.71-9.1l-8.11-22q-6.72.45-13.63.46t-13.63-.46l-8.11,22a16.18,16.18,0,0,1-21.71,9.1l-67-29.71a15.93,15.93,0,0,1-9.06-18.51L38,58A16.07,16.07,0,0,1,51,46.14l36.06-5.93a16.22,16.22,0,0,1,18.26,11.88l3.26,12.84Q118.11,64,128,64t19.4.93l3.26-12.84a16.21,16.21,0,0,1,18.26-11.88L205,46.14A16.07,16.07,0,0,1,218,58l29.53,116.38A15.93,15.93,0,0,1,238.45,192.9ZM232,178.28,202.47,62s0,0-.08,0L166.33,56a.17.17,0,0,0-.17,0l-2.83,11.14c5,.94,10,2.06,14.83,3.42A8,8,0,0,1,176,86.31a8.09,8.09,0,0,1-2.16-.3A172.25,172.25,0,0,0,128,80a172.25,172.25,0,0,0-45.84,6,8,8,0,1,1-4.32-15.4c4.82-1.36,9.78-2.48,14.82-3.42L89.83,56s0,0-.12,0h0L53.61,61.93a.17.17,0,0,0-.09,0L24,178.33,91,208a.23.23,0,0,0,.22,0L98,189.72a173.2,173.2,0,0,1-20.14-4.32A8,8,0,0,1,82.16,170,171.85,171.85,0,0,0,128,176a171.85,171.85,0,0,0,45.84-6,8,8,0,0,1,4.32,15.41A173.2,173.2,0,0,1,158,189.72L164.75,208a.22.22,0,0,0,.21,0Z" fill={color}></path></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg2382164700">
|
||||
<path d="M 15.418 19.037 L 3.44 3.637 C 3.311 3.471 3.288 3.247 3.381 3.058 C 3.473 2.87 3.665 2.75 3.875 2.75 L 6.148 2.75 C 6.318 2.75 6.478 2.829 6.582 2.963 L 18.56 18.363 C 18.689 18.529 18.712 18.753 18.619 18.942 C 18.527 19.13 18.335 19.25 18.125 19.25 L 15.852 19.25 C 15.682 19.25 15.522 19.171 15.418 19.037 Z" fill="transparent" strokeWidth="1.38" strokeMiterlimit="10" stroke={color}></path>
|
||||
<path d="M 18.333 2.75 L 3.667 19.25" fill="transparent" strokeWidth="1.38" strokeLinecap="round" strokeMiterlimit="10" stroke={color}></path>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export const GithubIcon2 = ({size = 'S', color = 'rgb(179, 179, 179)'}) => {
|
||||
let dimension = getSize(size);
|
||||
return <div style={{width: dimension, height: dimension}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" focusable="false" color={color}><g color={color}><path d="M208.31,75.68A59.78,59.78,0,0,0,202.93,28,8,8,0,0,0,196,24a59.75,59.75,0,0,0-48,24H124A59.75,59.75,0,0,0,76,24a8,8,0,0,0-6.93,4,59.78,59.78,0,0,0-5.38,47.68A58.14,58.14,0,0,0,56,104v8a56.06,56.06,0,0,0,48.44,55.47A39.8,39.8,0,0,0,96,192v8H72a24,24,0,0,1-24-24A40,40,0,0,0,8,136a8,8,0,0,0,0,16,24,24,0,0,1,24,24,40,40,0,0,0,40,40H96v16a8,8,0,0,0,16,0V192a24,24,0,0,1,48,0v40a8,8,0,0,0,16,0V192a39.8,39.8,0,0,0-8.44-24.53A56.06,56.06,0,0,0,216,112v-8A58.14,58.14,0,0,0,208.31,75.68ZM200,112a40,40,0,0,1-40,40H112a40,40,0,0,1-40-40v-8a41.74,41.74,0,0,1,6.9-22.48A8,8,0,0,0,80,73.83a43.81,43.81,0,0,1,.79-33.58,43.88,43.88,0,0,1,32.32,20.06A8,8,0,0,0,119.82,64h32.35a8,8,0,0,0,6.74-3.69,43.87,43.87,0,0,1,32.32-20.06A43.81,43.81,0,0,1,192,73.83a8.09,8.09,0,0,0,1,7.65A41.72,41.72,0,0,1,200,104Z" fill={color}></path></g></svg>
|
||||
</div>
|
||||
}
|
||||
7
packages/twenty-website/src/app/components/PostImage.tsx
Normal file
7
packages/twenty-website/src/app/components/PostImage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
export const PostImage = ({ sources, style }: { sources: { light: string, dark: string }, style?: React.CSSProperties }) => {
|
||||
return <Image src={sources.light} style={style} alt={sources.light} />
|
||||
}
|
||||
26
packages/twenty-website/src/app/layout.css
Normal file
26
packages/twenty-website/src/app/layout.css
Normal file
@ -0,0 +1,26 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
font-smooth: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(129, 129, 129);
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
@ -2,19 +2,22 @@ import type { Metadata } from 'next'
|
||||
import { Gabarito } from 'next/font/google'
|
||||
import EmotionRootStyleRegistry from './emotion-root-style-registry'
|
||||
import styled from '@emotion/styled'
|
||||
import { HeaderNav } from './components/HeaderNav'
|
||||
import { FooterNav } from './components/FooterNav'
|
||||
import { HeaderDesktop } from './components/HeaderDesktop'
|
||||
import { FooterDesktop } from './components/FooterDesktop'
|
||||
import { HeaderMobile } from '@/app/components/HeaderMobile'
|
||||
import './layout.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Twenty.dev',
|
||||
description: 'Twenty for Developer',
|
||||
icons: '/favicon.ico',
|
||||
icons: '/images/core/logo.svg',
|
||||
}
|
||||
|
||||
const gabarito = Gabarito({
|
||||
weight: ['400', '500'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
adjustFontFallback: false
|
||||
})
|
||||
|
||||
|
||||
@ -25,17 +28,14 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={gabarito.className}>
|
||||
<body style={{
|
||||
margin: 0,
|
||||
WebkitFontSmoothing: "antialiased",
|
||||
MozOsxFontSmoothing: "grayscale"
|
||||
}}>
|
||||
<body>
|
||||
<EmotionRootStyleRegistry>
|
||||
<HeaderNav />
|
||||
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
|
||||
<HeaderDesktop />
|
||||
<div className="container">
|
||||
<HeaderMobile />
|
||||
{children}
|
||||
</div>
|
||||
<FooterNav />
|
||||
<FooterDesktop />
|
||||
</EmotionRootStyleRegistry>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,22 +1,25 @@
|
||||
import { GetStaticProps } from 'next'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { compileMDX } from 'next-mdx-remote/rsc'
|
||||
import gfm from 'remark-gfm';
|
||||
import { ContentContainer } from '../components/ContentContainer';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import remarkBehead from 'remark-behead';
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
|
||||
interface Release {
|
||||
id: number;
|
||||
name: string;
|
||||
body: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const metadata: Metadata= {
|
||||
title: 'Twenty - Releases',
|
||||
description: 'Latest releases of Twenty',
|
||||
}
|
||||
|
||||
const Home = async () => {
|
||||
const res = await fetch(`${process.env.BASE_URL}/api/github`);
|
||||
const data: Release[] = await res.json();
|
||||
const response = await fetch('https://api.github.com/repos/twentyhq/twenty/releases');
|
||||
const data: Release[] = await response.json();
|
||||
|
||||
const releases = await Promise.all(
|
||||
data.map(async (release) => {
|
||||
@ -51,11 +54,11 @@ interface Release {
|
||||
<ContentContainer>
|
||||
<h1>Releases</h1>
|
||||
|
||||
{releases.map(release => (
|
||||
{releases.map((release, index) => (
|
||||
<div key={release.id}
|
||||
style={{
|
||||
padding: '24px 0px 24px 0px',
|
||||
borderBottom: '1px solid #ccc',
|
||||
borderBottom: index === releases.length - 1 ? 'none' : '1px solid #ccc',
|
||||
}}>
|
||||
<h2>{release.name}</h2>
|
||||
<div>{release.body}</div>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { getPost } from "@/app/user-guide/get-posts";
|
||||
|
||||
export default async function BlogPost({ params }: { params: { slug: string[] } }) {
|
||||
const post = await getPost(params.slug as string[]);
|
||||
console.log(post);
|
||||
|
||||
return <div>
|
||||
<h1>{post?.itemInfo.title}</h1>
|
||||
<div>{post?.content}</div>
|
||||
</div>;
|
||||
}
|
||||
97
packages/twenty-website/src/app/user-guide/get-posts.tsx
Normal file
97
packages/twenty-website/src/app/user-guide/get-posts.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { compileMDX } from 'next-mdx-remote/rsc';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface ItemInfo {
|
||||
title: string;
|
||||
position?: number;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
content: ReactElement;
|
||||
itemInfo: ItemInfo;
|
||||
}
|
||||
|
||||
export interface Directory {
|
||||
[key: string]: FileContent | Directory | ItemInfo;
|
||||
itemInfo: ItemInfo;
|
||||
}
|
||||
|
||||
const basePath = '/src/content/user-guide';
|
||||
|
||||
|
||||
async function getFiles(filePath: string, position: number = 0): Promise<Directory> {
|
||||
const entries = fs.readdirSync(filePath, { withFileTypes: true });
|
||||
|
||||
const urlpath = path.toString().split(basePath);
|
||||
const pathName = urlpath.length > 1 ? urlpath[1] : path.basename(filePath);
|
||||
console.log(pathName);
|
||||
|
||||
const directory: Directory = {
|
||||
itemInfo: {
|
||||
title: path.basename(filePath),
|
||||
position,
|
||||
type: 'directory',
|
||||
path: pathName,
|
||||
},
|
||||
};
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
directory[entry.name] = await getFiles(path.join(filePath, entry.name), position++);
|
||||
} else if (entry.isFile() && path.extname(entry.name) === '.mdx') {
|
||||
const fileContent = fs.readFileSync(path.join(filePath, entry.name), 'utf8');
|
||||
const { content, frontmatter } = await compileMDX<{ title: string, position?: number }>({ source: fileContent, options: { parseFrontmatter: true } });
|
||||
directory[entry.name] = { content, itemInfo: {...frontmatter, type: 'file', path: pathName + "/" + entry.name.replace(/\.mdx$/, '')} };
|
||||
}
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function parseFrontMatterAndCategory(directory: Directory, dirPath: string): Promise<Directory> {
|
||||
const parsedDirectory: Directory = {
|
||||
itemInfo: directory.itemInfo,
|
||||
};
|
||||
|
||||
for (const entry in directory) {
|
||||
if (entry !== 'itemInfo' && directory[entry] instanceof Object) {
|
||||
parsedDirectory[entry] = await parseFrontMatterAndCategory(directory[entry] as Directory, path.join(dirPath, entry));
|
||||
}
|
||||
}
|
||||
|
||||
const categoryPath = path.join(dirPath, '_category_.json');
|
||||
|
||||
if (fs.existsSync(categoryPath)) {
|
||||
const categoryJson: ItemInfo = JSON.parse(fs.readFileSync(categoryPath, 'utf8'));
|
||||
parsedDirectory.itemInfo = categoryJson;
|
||||
}
|
||||
|
||||
return parsedDirectory;
|
||||
}
|
||||
|
||||
export async function getPosts(): Promise<Directory> {
|
||||
const postsDirectory = path.join(process.cwd(), basePath);
|
||||
const directory = await getFiles(postsDirectory);
|
||||
return parseFrontMatterAndCategory(directory, postsDirectory);
|
||||
}
|
||||
|
||||
export async function getPost(slug: string[]): Promise<FileContent | null> {
|
||||
const postsDirectory = path.join(process.cwd(), basePath);
|
||||
const modifiedSlug = slug.join('/');
|
||||
const filePath = path.join(postsDirectory, `${modifiedSlug}.mdx`);
|
||||
|
||||
console.log(filePath);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const { content, frontmatter } = await compileMDX<{ title: string, position?: number }>({ source: fileContent, options: { parseFrontmatter: true } });
|
||||
|
||||
return { content, itemInfo: {...frontmatter, type: 'file', path: modifiedSlug }};
|
||||
}
|
||||
47
packages/twenty-website/src/app/user-guide/page.tsx
Normal file
47
packages/twenty-website/src/app/user-guide/page.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
import { getPosts, Directory, FileContent } from '@/app/user-guide/get-posts';
|
||||
import Link from 'next/link';
|
||||
|
||||
|
||||
const DirectoryItem = ({ name, item }: { name: string, item: Directory | FileContent }) => {
|
||||
if ('content' in item) {
|
||||
// If the item is a file, we render a link.
|
||||
return (
|
||||
<div key={name}>
|
||||
<Link href={`/user-guide/${item.itemInfo.path}`}>
|
||||
{item.itemInfo.title}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// If the item is a directory, we render the title and the items in the directory.
|
||||
return (
|
||||
<div key={name}>
|
||||
<h2>{item.itemInfo.title}</h2>
|
||||
{Object.entries(item).map(([childName, childItem]) => {
|
||||
if (childName !== 'itemInfo') {
|
||||
return <DirectoryItem key={childName} name={childName} item={childItem as Directory | FileContent} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default async function BlogHome() {
|
||||
|
||||
const posts = await getPosts();
|
||||
|
||||
|
||||
return <ContentContainer>
|
||||
<h1>User Guide</h1>
|
||||
<div>
|
||||
{Object.entries(posts).map(([name, item]) => {
|
||||
if (name !== 'itemInfo') {
|
||||
return <DirectoryItem key={name} name={name} item={item as Directory | FileContent} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</ContentContainer>;
|
||||
}
|
||||
Reference in New Issue
Block a user