From c422045ea62ea82303329c5e87492a909756c406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 29 Dec 2023 11:17:32 +0100 Subject: [PATCH] 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 --- packages/twenty-website/.env.example | 3 +- packages/twenty-website/.eslintrc.json | 8 +- packages/twenty-website/.gitignore | 3 + packages/twenty-website/.prettierrc | 5 + packages/twenty-website/next.config.js | 14 +- packages/twenty-website/package.json | 11 + .../src/app/components/AvatarGrid.tsx | 62 + .../src/app/components/ContentContainer.tsx | 32 +- .../src/app/components/ExternalArrow.tsx | 16 + .../src/app/components/FooterDesktop.tsx | 219 +- .../src/app/components/HeaderDesktop.tsx | 257 +- .../src/app/components/HeaderMobile.tsx | 288 +- .../src/app/components/Icons.tsx | 148 +- .../src/app/components/Logo.tsx | 23 +- .../src/app/components/Playground.tsx | 30 + .../app/components/PlaygroundTokenForm.tsx | 190 + .../src/app/components/PostImage.tsx | 16 +- .../contributors/api/[slug]/route.ts | 19 + .../contributors/api/generate/route.tsx | 288 + .../src/app/developers/contributors/page.tsx | 40 + .../src/app/developers/docs/graphql/page.tsx | 26 + .../src/app/developers/docs/layout.tsx | 63 + .../src/app/developers/docs/page.tsx | 9 + .../src/app/developers/docs/rest/page.tsx | 33 + .../src/app/developers/page.tsx | 9 + .../src/app/emotion-root-style-registry.js | 52 +- .../src/app/{user-guide => }/get-posts.tsx | 86 +- packages/twenty-website/src/app/layout.css | 11 + packages/twenty-website/src/app/layout.tsx | 40 +- packages/twenty-website/src/app/page.tsx | 11 +- .../twenty-website/src/app/releases/page.tsx | 114 +- .../src/app/user-guide/[...slug]/page.tsx | 11 - .../src/app/user-guide/[[...slug]]/layout.tsx | 120 + .../src/app/user-guide/[[...slug]]/page.tsx | 21 + .../src/app/user-guide/page.tsx | 47 - .../user-guide/basics/custom-objects.mdx | 5 +- .../src/content/user-guide/basics/notes.mdx | 8 +- .../user-guide/basics/opportunities.mdx | 7 +- .../src/content/user-guide/basics/tasks.mdx | 6 +- .../{basics/basics.mdx => home.mdx} | 11 +- .../integrations/connect-zapier.mdx | 14 +- .../integrations/generating-api-keys.mdx | 13 +- .../content/user-guide/others/glossary.mdx | 5 +- .../src/content/user-guide/others/tips.mdx | 3 +- packages/twenty-website/yarn.lock | 5105 ++++++++++++++++- yarn.lock | 774 ++- 46 files changed, 7589 insertions(+), 687 deletions(-) create mode 100644 packages/twenty-website/.prettierrc create mode 100644 packages/twenty-website/src/app/components/AvatarGrid.tsx create mode 100644 packages/twenty-website/src/app/components/ExternalArrow.tsx create mode 100644 packages/twenty-website/src/app/components/Playground.tsx create mode 100644 packages/twenty-website/src/app/components/PlaygroundTokenForm.tsx create mode 100644 packages/twenty-website/src/app/developers/contributors/api/[slug]/route.ts create mode 100644 packages/twenty-website/src/app/developers/contributors/api/generate/route.tsx create mode 100644 packages/twenty-website/src/app/developers/contributors/page.tsx create mode 100644 packages/twenty-website/src/app/developers/docs/graphql/page.tsx create mode 100644 packages/twenty-website/src/app/developers/docs/layout.tsx create mode 100644 packages/twenty-website/src/app/developers/docs/page.tsx create mode 100644 packages/twenty-website/src/app/developers/docs/rest/page.tsx create mode 100644 packages/twenty-website/src/app/developers/page.tsx rename packages/twenty-website/src/app/{user-guide => }/get-posts.tsx (50%) delete mode 100644 packages/twenty-website/src/app/user-guide/[...slug]/page.tsx create mode 100644 packages/twenty-website/src/app/user-guide/[[...slug]]/layout.tsx create mode 100644 packages/twenty-website/src/app/user-guide/[[...slug]]/page.tsx delete mode 100644 packages/twenty-website/src/app/user-guide/page.tsx rename packages/twenty-website/src/content/user-guide/{basics/basics.mdx => home.mdx} (93%) diff --git a/packages/twenty-website/.env.example b/packages/twenty-website/.env.example index e3001b2a0..9195d1d5b 100644 --- a/packages/twenty-website/.env.example +++ b/packages/twenty-website/.env.example @@ -1 +1,2 @@ -BASE_URL=http://localhost:3000 \ No newline at end of file +BASE_URL=http://localhost:3000 +GITHUB_TOKEN=your_github_token diff --git a/packages/twenty-website/.eslintrc.json b/packages/twenty-website/.eslintrc.json index bffb357a7..bd10e23f6 100644 --- a/packages/twenty-website/.eslintrc.json +++ b/packages/twenty-website/.eslintrc.json @@ -1,3 +1,7 @@ { - "extends": "next/core-web-vitals" -} + "extends": [ + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ] +} \ No newline at end of file diff --git a/packages/twenty-website/.gitignore b/packages/twenty-website/.gitignore index fd3dbb571..301c67f0c 100644 --- a/packages/twenty-website/.gitignore +++ b/packages/twenty-website/.gitignore @@ -34,3 +34,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# DB +*.sqlite \ No newline at end of file diff --git a/packages/twenty-website/.prettierrc b/packages/twenty-website/.prettierrc new file mode 100644 index 000000000..4510dc5c8 --- /dev/null +++ b/packages/twenty-website/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "auto" +} diff --git a/packages/twenty-website/next.config.js b/packages/twenty-website/next.config.js index 767719fc4..4c321604c 100644 --- a/packages/twenty-website/next.config.js +++ b/packages/twenty-website/next.config.js @@ -1,4 +1,14 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} -module.exports = nextConfig +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + }, + ], + }, +}; + +module.exports = nextConfig; diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index a2a51c3a8..15daac09e 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -11,20 +11,31 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@jsdevtools/rehype-toc": "^3.0.2", + "@stoplight/elements": "^7.16.2", "@tabler/icons-react": "^2.44.0", + "better-sqlite3": "^9.2.2", + "graphiql": "^3.0.10", "next": "14.0.4", "next-mdx-remote": "^4.4.1", "react": "^18", "react-dom": "^18", + "rehype-slug": "^6.0.0", "remark-behead": "^3.1.0", "remark-gfm": "^3.0.1" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.8", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^6.16.0", "eslint": "^8", "eslint-config-next": "14.0.4", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.2", + "eslint-plugin-react": "^7.33.2", + "prettier": "^3.1.1", "typescript": "^5" } } diff --git a/packages/twenty-website/src/app/components/AvatarGrid.tsx b/packages/twenty-website/src/app/components/AvatarGrid.tsx new file mode 100644 index 000000000..db2788eef --- /dev/null +++ b/packages/twenty-website/src/app/components/AvatarGrid.tsx @@ -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 ( + + {users.map(user => ( + + {user.login} + {user.login} + + ))} + + ); +}; + +export default AvatarGrid; diff --git a/packages/twenty-website/src/app/components/ContentContainer.tsx b/packages/twenty-website/src/app/components/ContentContainer.tsx index 0e6ba61a2..40d3b1228 100644 --- a/packages/twenty-website/src/app/components/ContentContainer.tsx +++ b/packages/twenty-website/src/app/components/ContentContainer.tsx @@ -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 ( - {children} - ) -} \ No newline at end of file +export const ContentContainer = ({ + children, +}: { + children?: React.ReactNode; +}) => { + return {children}; +}; diff --git a/packages/twenty-website/src/app/components/ExternalArrow.tsx b/packages/twenty-website/src/app/components/ExternalArrow.tsx new file mode 100644 index 000000000..dc0040115 --- /dev/null +++ b/packages/twenty-website/src/app/components/ExternalArrow.tsx @@ -0,0 +1,16 @@ +export const ExternalArrow = () => { + return ( +
+ + + + + +
+ ); +}; diff --git a/packages/twenty-website/src/app/components/FooterDesktop.tsx b/packages/twenty-website/src/app/components/FooterDesktop.tsx index 800bb73c8..9e2692aee 100644 --- a/packages/twenty-website/src/app/components/FooterDesktop.tsx +++ b/packages/twenty-website/src/app/components/FooterDesktop.tsx @@ -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 -
- - -
- The #1 Open Source CRM -
-
- - - Company - Pricing - Story - - - Resources - Documentation - Changelog - - - Other - OSS Friends - Terms of Service - Privacy Policy - - + const path = usePathname(); + const isTwentyDev = path.includes('developers'); + + if (isTwentyDev) return; + + return ( + +
+ + +
The #1 Open Source CRM
+
+ + + Company + Pricing + Story + + + Resources + + Documentation + + + Changelog + + + + Other + + OSS Friends + + + Terms of Service + + + Privacy Policy + + + +
+
+
+ © + 2023 Twenty PBC
-
-
- © - 2023 Twenty PBC -
- + justifyContent: 'space-between', + gap: '10px', + }} + > + + + + + + + + + + + +
+
- ; -} + ); +}; diff --git a/packages/twenty-website/src/app/components/HeaderDesktop.tsx b/packages/twenty-website/src/app/components/HeaderDesktop.tsx index c93cec27e..0220dc42e 100644 --- a/packages/twenty-website/src/app/components/HeaderDesktop.tsx +++ b/packages/twenty-website/src/app/components/HeaderDesktop.tsx @@ -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 - Sign in - - - Get Started - - - ; + const path = usePathname(); + const isTwentyDev = path.includes('developers'); + + return ( + + {isTwentyDev ? ( + <> +
+ + + + + + + + + +
+ + ) : ( + <> + + Sign in + + + Get Started + + + )} +
+ ); }; - -const ExternalArrow = () => { - return
- -
- -} - export const HeaderDesktop = () => { + const path = usePathname(); + const isTwentyDev = path.includes('developers'); - const isTwentyDev = false; - - return + ); }; - diff --git a/packages/twenty-website/src/app/components/HeaderMobile.tsx b/packages/twenty-website/src/app/components/HeaderMobile.tsx index d33e47ca7..3552ada43 100644 --- a/packages/twenty-website/src/app/components/HeaderMobile.tsx +++ b/packages/twenty-website/src/app/components/HeaderMobile.tsx @@ -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 - Sign in - - - Get Started - - - ; + return ( + + + Sign in + + + Get Started + + + ); }; - -const ExternalArrow = () => { - return
- -
-} - 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 ; + + + ); }; - diff --git a/packages/twenty-website/src/app/components/Icons.tsx b/packages/twenty-website/src/app/components/Icons.tsx index e3659fe29..b851db6cb 100644 --- a/packages/twenty-website/src/app/components/Icons.tsx +++ b/packages/twenty-website/src/app/components/Icons.tsx @@ -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
- +export const GithubIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => { + const dimension = getSize(size); + return ( +
+ + +
-} + ); +}; -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
- -
; -} - -export const DiscordIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => { - let dimension = getSize(size); - return
- + return ( +
+ + + + +
-} + ); +}; -export const XIcon = ({size = 'S', color = 'rgb(179, 179, 179)'}) => { - let dimension = getSize(size); - return
- - - - +export const DiscordIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => { + const dimension = getSize(size); + return ( +
+ + + + +
-} + ); +}; -export const GithubIcon2 = ({size = 'S', color = 'rgb(179, 179, 179)'}) => { - let dimension = getSize(size); - return
- +export const XIcon = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => { + const dimension = getSize(size); + return ( +
+ + + +
-} \ No newline at end of file + ); +}; + +export const GithubIcon2 = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => { + const dimension = getSize(size); + return ( +
+ + + + + +
+ ); +}; diff --git a/packages/twenty-website/src/app/components/Logo.tsx b/packages/twenty-website/src/app/components/Logo.tsx index 1d1310f5c..30a81ed46 100644 --- a/packages/twenty-website/src/app/components/Logo.tsx +++ b/packages/twenty-website/src/app/components/Logo.tsx @@ -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 ; + return ; }; diff --git a/packages/twenty-website/src/app/components/Playground.tsx b/packages/twenty-website/src/app/components/Playground.tsx new file mode 100644 index 000000000..b6ba35d0f --- /dev/null +++ b/packages/twenty-website/src/app/components/Playground.tsx @@ -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 ( + <> + + {isTokenValid && children} + + ); +}; + +export default Playground; diff --git a/packages/twenty-website/src/app/components/PlaygroundTokenForm.tsx b/packages/twenty-website/src/app/components/PlaygroundTokenForm.tsx new file mode 100644 index 000000000..251df5054 --- /dev/null +++ b/packages/twenty-website/src/app/components/PlaygroundTokenForm.tsx @@ -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) => { + 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 && ( + +
+ +

+ + + Token invalid + + + + +

+
+
+ ) + ); +}; + +export default TokenForm; diff --git a/packages/twenty-website/src/app/components/PostImage.tsx b/packages/twenty-website/src/app/components/PostImage.tsx index 58ee96c2e..b5bc150d7 100644 --- a/packages/twenty-website/src/app/components/PostImage.tsx +++ b/packages/twenty-website/src/app/components/PostImage.tsx @@ -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 {sources.light} -} \ No newline at end of file +export const PostImage = ({ + sources, + style, +}: { + sources: { light: string; dark: string }; + style?: React.CSSProperties; +}) => { + return {sources.light}; +}; diff --git a/packages/twenty-website/src/app/developers/contributors/api/[slug]/route.ts b/packages/twenty-website/src/app/developers/contributors/api/[slug]/route.ts new file mode 100644 index 000000000..26e1f7aae --- /dev/null +++ b/packages/twenty-website/src/app/developers/contributors/api/[slug]/route.ts @@ -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); +} diff --git a/packages/twenty-website/src/app/developers/contributors/api/generate/route.tsx b/packages/twenty-website/src/app/developers/contributors/api/generate/route.tsx new file mode 100644 index 000000000..25c31915b --- /dev/null +++ b/packages/twenty-website/src/app/developers/contributors/api/generate/route.tsx @@ -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 = []): Promise> { + const { repository } = await query(` + 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 = [...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> { + const { repository } = await query(` + 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; + const issues = await fetchData(lastIssueCursor) as Array; + + 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 }); +}; diff --git a/packages/twenty-website/src/app/developers/contributors/page.tsx b/packages/twenty-website/src/app/developers/contributors/page.tsx new file mode 100644 index 000000000..ec2ee6619 --- /dev/null +++ b/packages/twenty-website/src/app/developers/contributors/page.tsx @@ -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 ( +
+

Top Contributors

+ +
+ ); +}; + +export default Contributors; diff --git a/packages/twenty-website/src/app/developers/docs/graphql/page.tsx b/packages/twenty-website/src/app/developers/docs/graphql/page.tsx new file mode 100644 index 000000000..5a5fd713b --- /dev/null +++ b/packages/twenty-website/src/app/developers/docs/graphql/page.tsx @@ -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 ; +} + +// Dynamically import the GraphiQL component with SSR disabled +const GraphiQLWithNoSSR = dynamic(() => Promise.resolve(GraphiQLComponent), { + ssr: false, +}); + +const GraphQLDocs = () => { + return ; +}; + +export default GraphQLDocs; diff --git a/packages/twenty-website/src/app/developers/docs/layout.tsx b/packages/twenty-website/src/app/developers/docs/layout.tsx new file mode 100644 index 000000000..88c54011b --- /dev/null +++ b/packages/twenty-website/src/app/developers/docs/layout.tsx @@ -0,0 +1,63 @@ +import { ContentContainer } from '@/app/components/ContentContainer'; + +const DeveloperDocsLayout = ({ children }: { children: React.ReactNode }) => { + return ( + +
+
+

+ Install & Maintain +

+ + Local setup + {' '} +
+ + Self-hosting + {' '} +
+ + Upgrade guide + {' '} +

+

+ Resources +

+ + Contributors Guide + {' '} +
+ + GraphQL API + {' '} +
+ + Rest API + + + Twenty UI + {' '} +
+
+
+ {children} +
+
+
+ ); +}; + +export default DeveloperDocsLayout; diff --git a/packages/twenty-website/src/app/developers/docs/page.tsx b/packages/twenty-website/src/app/developers/docs/page.tsx new file mode 100644 index 000000000..51b49eaf2 --- /dev/null +++ b/packages/twenty-website/src/app/developers/docs/page.tsx @@ -0,0 +1,9 @@ +const DeveloperDocs = () => { + return ( +
+

Developer Docs

+
+ ); +}; + +export default DeveloperDocs; diff --git a/packages/twenty-website/src/app/developers/docs/rest/page.tsx b/packages/twenty-website/src/app/developers/docs/rest/page.tsx new file mode 100644 index 000000000..cb90e5f04 --- /dev/null +++ b/packages/twenty-website/src/app/developers/docs/rest/page.tsx @@ -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 ( + + ); +};*/ + +const RestApi = () => { + /* const [openApiJson, setOpenApiJson] = useState({}); + + const children = ;*/ + + return <>API; + + // return {children}; +}; + +export default RestApi; diff --git a/packages/twenty-website/src/app/developers/page.tsx b/packages/twenty-website/src/app/developers/page.tsx new file mode 100644 index 000000000..81c5803ee --- /dev/null +++ b/packages/twenty-website/src/app/developers/page.tsx @@ -0,0 +1,9 @@ +const Developers = () => { + return ( +
+

This page should probably be built on Framer

+
+ ); +}; + +export default Developers; diff --git a/packages/twenty-website/src/app/emotion-root-style-registry.js b/packages/twenty-website/src/app/emotion-root-style-registry.js index d7901367c..bf801d059 100644 --- a/packages/twenty-website/src/app/emotion-root-style-registry.js +++ b/packages/twenty-website/src/app/emotion-root-style-registry.js @@ -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 (