diff --git a/website/content/docs/intro.mdx b/website/content/docs/intro.mdx index 1a5bbb68..bb5d7e50 100644 --- a/website/content/docs/intro.mdx +++ b/website/content/docs/intro.mdx @@ -8,14 +8,14 @@ Static CMS is an open source content management system for your Git workflow tha At its core, Static CMS is an open-source React app that acts as a wrapper for the Git workflow, using the GitHub, GitLab, or Bitbucket API. This provides many advantages, including: -* **Fast, web-based UI:** With rich-text editing, real-time preview, and drag-and-drop media uploads. -* **Platform agnostic:** Works with most static site generators. -* **Easy installation:** Add two files to your site and hook up the backend by including those files in your build process or linking to our Content Delivery Network (CDN). -* **Modern authentication:** Using GitHub, GitLab, or Bitbucket and JSON web tokens. -* **Flexible content types:** Specify an unlimited number of content types with custom fields. -* **Fully extensible:** Create custom-styled previews, UI widgets, and editor plugins. +- **Fast, web-based UI:** With rich-text editing, real-time preview, and drag-and-drop media uploads. +- **Platform agnostic:** Works with most static site generators. +- **Easy installation:** Add two files to your site and hook up the backend by including those files in your build process or linking to our Content Delivery Network (CDN). +- **Modern authentication:** Using GitHub, GitLab, or Bitbucket and JSON web tokens. +- **Flexible content types:** Specify an unlimited number of content types with custom fields. +- **Fully extensible:** Create custom-styled previews, UI widgets, and editor plugins. -### Find out more +## Find out more - Get a feel for the UI in the [demo site](https://cms-demo.netlify.com). (No login required. Click the login button to go straight to the CMS editor UI.) - [Start with a template](/docs/start-with-a-template/) to make a Static CMS-enabled site of your own. diff --git a/website/content/docs/site-generator-overview.mdx b/website/content/docs/site-generator-overview.mdx index 834587a1..3ff4c753 100644 --- a/website/content/docs/site-generator-overview.mdx +++ b/website/content/docs/site-generator-overview.mdx @@ -6,29 +6,29 @@ weight: 1 The process for adding Static CMS to a static site can be divided into four main steps: -### Install Static CMS +## Install Static CMS This is a single page app available at the `/admin` route of your website. Check out the [general overview](/docs/intro/) to see what the installation process entails. -### Set up a backend +## Set up a backend This serves two purpose: Secure access to your website's Static CMS and allows it to read and update content files in your repo. More information about configuring the backend can be found [here](/docs/backends-overview/). -### Configure Static CMS using a configuration file +## Configure Static CMS using a configuration file For starters, you can get by with a basic configuration that includes required information like Git provider, branch and folders to save files to. Once you've gotten the hang of it, you can use the file to build whatever collections and content modeling you want. Check out the [this section](/docs/configuration-options/#configuration-file) for full details about all available configuration options. -### Render the content provided by Static CMS as web pages +## Render the content provided by Static CMS as web pages Static CMS manages your content, and provides admin features, but it doesn't deliver content. It only makes your content available through an API. It is up to developers to determine how to build the raw content into something useful and delightful on the frontend. To learn how to query raw content managed by Static CMS and reformat them for delivery to end users, please refer the dedicated section for your site generator in the Table of Content. -___ -### Local development + +## Local development If you are experimenting with Static CMS or testing things out, you can connect it to a local Git repository instead of a live one. Learn how to do it [here](/docs/beta-features/#working-with-a-local-git-repository). diff --git a/website/src/components/community/CommunitySection.tsx b/website/src/components/community/CommunitySection.tsx index fbc304cd..7d7ab348 100644 --- a/website/src/components/community/CommunitySection.tsx +++ b/website/src/components/community/CommunitySection.tsx @@ -23,7 +23,7 @@ interface CommunitySectionProps { const CommunitySection = ({ section }: CommunitySectionProps) => { return ( - + {section.title} diff --git a/website/src/components/docs/DocsContent.tsx b/website/src/components/docs/DocsContent.tsx index abc170d8..6c6e54b6 100644 --- a/website/src/components/docs/DocsContent.tsx +++ b/website/src/components/docs/DocsContent.tsx @@ -10,6 +10,14 @@ const DocsContent = styled('div')( display: flex; flex-direction: column; + ${theme.breakpoints.between('sm', 'lg')} { + padding: 0 40px; + } + + ${theme.breakpoints.down('sm')} { + padding: 0 32px; + } + & time { color: #9b9b9b; } @@ -54,7 +62,7 @@ const DocsContent = styled('div')( color: ${theme.palette.text.primary}; } - @media (min-width: 1200px) { + ${theme.breakpoints.up('lg')} { & h1 { margin-top: 16px; margin-bottom: 16px; @@ -66,13 +74,13 @@ const DocsContent = styled('div')( line-height: 36px; } - @media (min-width: 600px) and (max-width: 1200px) { + ${theme.breakpoints.between('sm', 'lg')} { & h1 { font-size: 26px; } } - @media (max-width: 600px) { + ${theme.breakpoints.down('sm')} { & h1 { font-size: 24px; } @@ -84,13 +92,13 @@ const DocsContent = styled('div')( line-height: 26px; } - @media (min-width: 600px) and (max-width: 1200px) { + ${theme.breakpoints.between('sm', 'lg')} { & h2 { font-size: 20px; } } - @media (max-width: 600px) { + ${theme.breakpoints.down('sm')} { & h2 { font-size: 18px; } @@ -102,13 +110,13 @@ const DocsContent = styled('div')( line-height: 24px; } - @media (min-width: 600px) and (max-width: 1200px) { + ${theme.breakpoints.between('sm', 'lg')} { & h3 { font-size: 18px; } } - @media (max-width: 600px) { + ${theme.breakpoints.down('sm')} { & h3 { font-size: 17px; } @@ -119,7 +127,7 @@ const DocsContent = styled('div')( line-height: 20px; } - @media (max-width: 1200px) { + ${theme.breakpoints.down('lg')} { & h4 { font-size: 16px; } @@ -130,7 +138,7 @@ const DocsContent = styled('div')( line-height: 19px; } - @media (max-width: 1200px) { + ${theme.breakpoints.down('lg')} { & h5 { font-size: 15px; } @@ -141,7 +149,7 @@ const DocsContent = styled('div')( line-height: 18px; } - @media (max-width: 1200px) { + ${theme.breakpoints.down('lg')} { & h6 { font-size: 14px; } @@ -295,7 +303,7 @@ const DocsContent = styled('div')( max-width: 100%; } - @media (min-width: 800px) { + ${theme.breakpoints.down('md')} { & h2, & h3, & h4, diff --git a/website/src/components/docs/DocsLeftNav.tsx b/website/src/components/docs/DocsLeftNav.tsx index 954de9cf..bf4da19b 100644 --- a/website/src/components/docs/DocsLeftNav.tsx +++ b/website/src/components/docs/DocsLeftNav.tsx @@ -1,4 +1,5 @@ import List from '@mui/material/List'; +import { useTheme } from '@mui/material/styles'; import DocsLeftNavGroup from './DocsLeftNavGroup'; @@ -9,6 +10,8 @@ export interface DocsLeftNavProps { } const DocsLeftNav = ({ docsGroups }: DocsLeftNavProps) => { + const theme = useTheme(); + return ( { bottom: 0, overflowY: 'auto', paddingBottom: '24px', + [theme.breakpoints.down('lg')]: { + display: 'none', + }, }} dense > diff --git a/website/src/components/docs/components/Header2.tsx b/website/src/components/docs/components/Header2.tsx index 6125cb21..a2f86b30 100644 --- a/website/src/components/docs/components/Header2.tsx +++ b/website/src/components/docs/components/Header2.tsx @@ -1,5 +1,6 @@ import LinkIcon from '@mui/icons-material/Link'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import Link from 'next/link'; import type { ReactNode } from 'react'; @@ -8,10 +9,14 @@ const StyledLink = styled('a')( ({ theme }) => ` position: absolute; margin-left: -28px; - top: 0; + top: -1px; font-weight: 300; color: ${theme.palette.secondary.main}; - transform: rotateZ(-45deg) + transform: rotateZ(-45deg); + + ${theme.breakpoints.down('sm')} { + margin-left: -22px; + } `, ); @@ -29,15 +34,34 @@ interface Header2Props { const Header2 = ({ children = '' }: Header2Props) => { const anchor = getAnchor(String(children)); const link = `#${anchor}`; + const theme = useTheme(); return ( -

+ - + {children} -

+
); }; diff --git a/website/src/components/docs/components/Header3.tsx b/website/src/components/docs/components/Header3.tsx index 5573724d..0506782d 100644 --- a/website/src/components/docs/components/Header3.tsx +++ b/website/src/components/docs/components/Header3.tsx @@ -1,5 +1,6 @@ import LinkIcon from '@mui/icons-material/Link'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; +import Typography from '@mui/material/Typography'; import Link from 'next/link'; import type { ReactNode } from 'react'; @@ -11,7 +12,12 @@ const StyledLink = styled('a')( top: 0; font-weight: 300; color: ${theme.palette.text.primary}; - transform: rotateZ(-45deg) + transform: rotateZ(-45deg); + + ${theme.breakpoints.down('sm')} { + margin-left: -22px; + top: -1px; + } `, ); @@ -29,15 +35,34 @@ interface Header3Props { const Header3 = ({ children = '' }: Header3Props) => { const anchor = getAnchor(String(children)); const link = `#${anchor}`; + const theme = useTheme(); return ( -

+ - + {children} -

+ ); }; diff --git a/website/src/components/docs/table_of_contents/DocsHeadings.tsx b/website/src/components/docs/table_of_contents/DocsHeadings.tsx index 4d76f210..c8dba76d 100644 --- a/website/src/components/docs/table_of_contents/DocsHeadings.tsx +++ b/website/src/components/docs/table_of_contents/DocsHeadings.tsx @@ -2,12 +2,18 @@ import { styled } from '@mui/material/styles'; import type { NestedHeading } from './DocsTableOfContents'; -const StyledList = styled('ul')` - display: flex; - flex-direction: column; - list-style-type: none; - padding: 0; -`; +const StyledList = styled('ul')( + ({ theme }) => ` + display: flex; + flex-direction: column; + list-style-type: none; + padding: 0; + + ${theme.breakpoints.down('lg')} { + margin-top: 0; + } + `, +); const StyledListItem = styled('li')( ({ theme }) => ` diff --git a/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx b/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx index f1af5d93..34a325ed 100644 --- a/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx +++ b/website/src/components/docs/table_of_contents/DocsTableOfContents.tsx @@ -38,8 +38,8 @@ const useHeadingsData = () => { useEffect(() => { const headingElements = Array.from( - document.querySelectorAll('main h2, main h3'), - ) as HTMLHeadingElement[]; + document.querySelectorAll('main h2, main h3'), + ); // Created a list of headings, with H3s nested const newNestedHeadings = getNestedHeadings(headingElements); @@ -50,10 +50,10 @@ const useHeadingsData = () => { }; const useIntersectionObserver = (setActiveId: (activeId: string) => void) => { - const headingElementsRef = useRef({}); + const headingElementsRef = useRef>({}); const { asPath } = useRouter(); useEffect(() => { - const headingElements = Array.from(document.querySelectorAll('h2, h3')); + const headingElements = Array.from(document.querySelectorAll('h2, h3')); if (headingElementsRef.current) { headingElementsRef.current = {}; @@ -105,16 +105,26 @@ const useIntersectionObserver = (setActiveId: (activeId: string) => void) => { }, [setActiveId, asPath]); }; -const StyledNav = styled('nav')` - width: 100%; - padding: 0 16px 16px 0; - align-self: flex-start; - position: sticky; - top: 0; - max-height: calc(100vh - 72px); - overflow-y: auto; - top: 16px; -`; +const StyledNav = styled('nav')( + ({ theme }) => ` + width: 100%; + padding: 0 16px 16px 0; + align-self: flex-start; + position: sticky; + top: 0; + max-height: calc(100vh - 72px); + overflow-y: auto; + top: 16px; + + ${theme.breakpoints.between('md', 'lg')} { + top: 0; + } + + ${theme.breakpoints.down('md')} { + display: none; + } + `, +); const DocsTableOfContents = () => { const [activeId, setActiveId] = useState(); diff --git a/website/src/components/layout/Container.tsx b/website/src/components/layout/Container.tsx index 0973dc84..d0aff17e 100644 --- a/website/src/components/layout/Container.tsx +++ b/website/src/components/layout/Container.tsx @@ -2,14 +2,20 @@ import { styled } from '@mui/material/styles'; import type { ReactNode } from 'react'; -const StyledContainer = styled('div')` - max-width: 1280px; - width: 100%; - padding: 0 40px; - display: flex; - flex-direction: column; - align-items: center; -`; +const StyledContainer = styled('div')( + ({ theme }) => ` + max-width: 1280px; + width: 100%; + padding: 0 40px; + display: flex; + flex-direction: column; + align-items: center; + + ${theme.breakpoints.down('md')} { + padding: 0 32px; + } + `, +); export interface PageProps { children: ReactNode; diff --git a/website/src/components/layout/Header.tsx b/website/src/components/layout/Header.tsx index 9c347aa5..a26b69d2 100644 --- a/website/src/components/layout/Header.tsx +++ b/website/src/components/layout/Header.tsx @@ -1,19 +1,23 @@ +import Brightness4Icon from '@mui/icons-material/Brightness4'; +import Brightness7Icon from '@mui/icons-material/Brightness7'; import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; import AppBar from '@mui/material/AppBar'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import { styled } from '@mui/material/styles'; -import TextField from '@mui/material/TextField'; import Toolbar from '@mui/material/Toolbar'; -import Image from 'next/image'; import Link from 'next/link'; -import Brightness4Icon from '@mui/icons-material/Brightness4'; -import Brightness7Icon from '@mui/icons-material/Brightness7'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import { useCallback, useMemo, useState } from 'react'; +import Logo from './Logo'; +import NavigationDrawer from './mobile-drawer/NavigationDrawer'; +import Search from './Search'; + +import type { PaletteMode } from '@mui/material'; import type { ButtonTypeMap } from '@mui/material/Button'; import type { ExtendButtonBase } from '@mui/material/ButtonBase'; -import type { PaletteMode } from '@mui/material'; +import type { DocsGroup, MenuItem } from '../../interface'; const StyledAppBar = styled(AppBar)( ({ theme }) => ` @@ -21,17 +25,47 @@ const StyledAppBar = styled(AppBar)( `, ); -const StyledToolbar = styled(Toolbar)` - gap: 16px; -`; +const StyledToolbar = styled(Toolbar)( + ({ theme }) => ` + gap: 16px; + height: 72px; -const StyledGithubLink = styled('a')` + ${theme.breakpoints.down('lg')} { + justify-content: space-between; + } + `, +); + +const StyledIconsWrapper = styled('div')( + ({ theme }) => ` + display: flex; + align-items: center; + justify-content: center; + + ${theme.breakpoints.up('lg')} { + flex-grow: 1; + } + `, +); + +const StyledGithubLink = styled('a')( + ({ theme }) => ` + display: flex; + align-items: center; + + ${theme.breakpoints.down('lg')} { + display: none; + } + `, +); + +const StyledGithubImage = styled('img')` display: flex; `; const StyledMenuButton = styled(IconButton)( ({ theme }) => ` - ${theme.breakpoints.up('md')} { + ${theme.breakpoints.up('lg')} { visibility: hidden; height: 0; width: 0; @@ -40,102 +74,136 @@ const StyledMenuButton = styled(IconButton)( `, ); -const StyledGap = styled('div')` - flex-grow: 1; -`; +const StyledDesktopGap = styled('div')( + ({ theme }) => ` + flex-grow: 1; -const StyledSearchBox = styled(TextField)` - background-color: rgba(255, 255, 255, 0.1); - border-radius: 4px; + ${theme.breakpoints.down('lg')} { + display: none; + } + `, +); - .MuiSvgIcon-root { - color: rgba(255, 255, 255, 0.8); - } - - .MuiInputBase-root { +const StyledDesktopLink = styled(Button)( + ({ theme }) => ` color: white; - } - .MuiOutlinedInput-notchedOutline { - border: none; - } -`; + &:hover { + color: rgba(255, 255, 255, 0.6); + } -const StyledLink = styled(Button)` - color: white; - - &:hover { - color: rgba(255, 255, 255, 0.6); - } -` as ExtendButtonBase>; - -const StyledImageLink = styled('a')` - display: flex; - align-items: center; -`; - -const StyledImage = styled(Image)` - cursor: pointer; -`; + ${theme.breakpoints.down('lg')} { + display: none; + } + `, +) as ExtendButtonBase>; interface HeaderProps { mode: PaletteMode; + docsGroups: DocsGroup[]; toggleColorMode: () => void; } -const Header = ({ mode, toggleColorMode }: HeaderProps) => { +const Header = ({ mode, docsGroups, toggleColorMode }: HeaderProps) => { + const [mobileOpen, setMobileOpen] = useState(false); + + const handleDrawerToggle = useCallback(() => { + setMobileOpen(!mobileOpen); + }, [mobileOpen]); + + const items: MenuItem[] = useMemo( + () => [ + { + title: 'Docs', + path: '/docs', + groups: docsGroups.map(group => ({ + title: group.title, + links: group.links.map(link => ({ + title: link.title, + url: `/docs/${link.slug}`, + })), + })), + }, + { + title: 'Contributing', + url: '/docs/contributor-guide', + }, + { + title: 'Community', + url: '/community', + }, + ], + [docsGroups], + ); + return ( - - - - - - - - - - - , - }} - /> - - {mode === 'dark' ? : } - - - - {/* eslint-disable-next-line @next/next/no-img-element */} - Star StaticJsCMS/static-cms on GitHub - - - Docs - - - Contributing - - - Community - - {/* + <> + + + + + + + + + + {mode === 'dark' ? : } + + + + {/* eslint-disable-next-line @next/next/no-img-element */} + + + + + + + {items.map(item => { + let url = '#'; + if ('url' in item) { + url = item.url; + } else if (item.groups.length > 0 && item.groups[0].links.length > 0) { + url = item.groups[0].links[0].url; + } + + return ( + + {item.title} + + ); + })} + {/* Blog */} - - + + + + ); }; diff --git a/website/src/components/layout/Logo.tsx b/website/src/components/layout/Logo.tsx new file mode 100644 index 00000000..a3f49181 --- /dev/null +++ b/website/src/components/layout/Logo.tsx @@ -0,0 +1,51 @@ +import { styled } from '@mui/material/styles'; +import Image from 'next/image'; +import Link from 'next/link'; + +import transientOptions from '../../util/transientOptions'; + +interface StyledImageLinkProps { + $inDrawer: boolean; +} + +const StyledImageLink = styled( + 'a', + transientOptions, +)( + ({ theme, $inDrawer }) => ` + display: flex; + align-items: center; + + ${ + !$inDrawer + ? ` + ${theme.breakpoints.down('lg')} { + position: absolute; + left: 50%; + transform: translate(-50%, 0); + } + ` + : '' + } + `, +); + +const StyledImage = styled(Image)` + cursor: pointer; +`; + +interface LogoProps { + inDrawer?: boolean; +} + +const Logo = ({ inDrawer = false }: LogoProps) => { + return ( + + + + + + ); +}; + +export default Logo; diff --git a/website/src/components/layout/Page.tsx b/website/src/components/layout/Page.tsx index 149672e4..a1073c39 100644 --- a/website/src/components/layout/Page.tsx +++ b/website/src/components/layout/Page.tsx @@ -12,6 +12,7 @@ import Container from './Container'; import Header from './Header'; import type { ReactNode } from 'react'; +import type { DocsGroup } from '../../interface'; const StyledPageContentWrapper = styled('div')` display: flex; @@ -35,6 +36,7 @@ export interface PageProps { image?: string; }; fullWidth?: boolean; + docsGroups: DocsGroup[]; } const Page = ({ @@ -45,6 +47,7 @@ const Page = ({ description, pageDetails, fullWidth = false, + docsGroups, }: PageProps) => { const scrollableArea = useRef(null); const theme = useTheme(); @@ -89,7 +92,11 @@ const Page = ({ description={description} /> ) : null} -
+
{content} ); diff --git a/website/src/components/layout/Search.tsx b/website/src/components/layout/Search.tsx new file mode 100644 index 00000000..c2269ccf --- /dev/null +++ b/website/src/components/layout/Search.tsx @@ -0,0 +1,41 @@ +import SearchIcon from '@mui/icons-material/Search'; +import { styled } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; + +const StyledSearchBox = styled(TextField)( + ({ theme }) => ` + background-color: rgba(255, 255, 255, 0.1); + border-radius: 4px; + + .MuiSvgIcon-root { + color: rgba(255, 255, 255, 0.8); + } + + .MuiInputBase-root { + color: white; + } + + .MuiOutlinedInput-notchedOutline { + border: none; + } + + ${theme.breakpoints.down('lg')} { + display: none; + } + `, +); + +const Search = () => { + return ( + , + }} + /> + ); +}; + +export default Search; diff --git a/website/src/components/layout/mobile-drawer/MobileNavItem.tsx b/website/src/components/layout/mobile-drawer/MobileNavItem.tsx new file mode 100644 index 00000000..69679355 --- /dev/null +++ b/website/src/components/layout/mobile-drawer/MobileNavItem.tsx @@ -0,0 +1,133 @@ +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import Collapse from '@mui/material/Collapse'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import { useTheme } from '@mui/material/styles'; +import Link from 'next/link'; +import { useCallback, useMemo, useState } from 'react'; +import ListSubheader from '@mui/material/ListSubheader'; +import { useRouter } from 'next/router'; + +import MobileNavLink from './MobileNavLink'; + +import type { MouseEvent } from 'react'; +import type { MenuItem, MenuLink, MenuLinkGroup } from '../../../interface'; + +interface MobileNavItemProps { + item: MenuItem; +} + +function isMenuLinkGroup(link: MenuItem): link is MenuLinkGroup { + return 'groups' in link; +} + +const MobileNavItem = ({ item }: MobileNavItemProps) => { + const theme = useTheme(); + const { asPath } = useRouter(); + + const selected = useMemo(() => { + if ('url' in item) { + return asPath === item.url; + } + + return asPath.startsWith(item.path); + }, [asPath, item]); + + const [open, setOpen] = useState(selected); + + const handleOnClick = useCallback( + (link: MenuItem | MenuLink) => (event: MouseEvent) => { + if (isMenuLinkGroup(link)) { + event.stopPropagation(); + setOpen(!open); + return; + } + }, + [open], + ); + + const url = useMemo(() => { + if (isMenuLinkGroup(item)) { + return undefined; + } + + return item.url; + }, [item]); + + const wrappedLink = useMemo(() => { + const button = ( + + + {isMenuLinkGroup(item) ? ( + + ) : null} + + ); + + if (!url) { + return button; + } + + return ( + + {button} + + ); + }, [handleOnClick, item, open, selected, theme.transitions, url]); + + return ( + <> + {wrappedLink} + {isMenuLinkGroup(item) ? ( + + {item.groups.map(group => ( + + {group.title} + + } + disablePadding + sx={{ + marginTop: '8px', + '&:not(:first-child)': { + marginTop: '20px', + }, + }} + > + {group.links.map(link => ( + + ))} + + ))} + + ) : null} + + ); +}; + +export default MobileNavItem; diff --git a/website/src/components/layout/mobile-drawer/MobileNavLink.tsx b/website/src/components/layout/mobile-drawer/MobileNavLink.tsx new file mode 100644 index 00000000..cfa8138b --- /dev/null +++ b/website/src/components/layout/mobile-drawer/MobileNavLink.tsx @@ -0,0 +1,36 @@ +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import { useMemo } from 'react'; + +import type { MouseEvent } from 'react'; +import type { MenuLink } from '../../../interface'; + +interface MobileNavLinkProps { + link: MenuLink; + onClick: (event: MouseEvent) => void; +} + +const MobileNavLink = ({ link, onClick }: MobileNavLinkProps) => { + const { title, url } = link; + const { asPath } = useRouter(); + + const selected = useMemo(() => { + return asPath === url; + }, [asPath, url]); + + return ( + + + + + + ); +}; + +export default MobileNavLink; diff --git a/website/src/components/layout/mobile-drawer/NavigationDrawer.tsx b/website/src/components/layout/mobile-drawer/NavigationDrawer.tsx new file mode 100644 index 00000000..17f0de46 --- /dev/null +++ b/website/src/components/layout/mobile-drawer/NavigationDrawer.tsx @@ -0,0 +1,99 @@ +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import { styled, useTheme } from '@mui/material/styles'; +import SwipeableDrawer from '@mui/material/SwipeableDrawer'; +import { useMemo } from 'react'; + +import MobileNavItem from './MobileNavItem'; +import Logo from '../Logo'; + +import type { MenuItem } from '../../../interface'; + +const DRAWER_WIDTH = 300; + +const StyledDrawerContents = styled('div')` + padding-top: 16px; + text-align: center; +`; + +const StyledLogoWrapper = styled('div')` + display: flex; + justify-content: center; +`; + +interface NavigationDrawerProps { + items: MenuItem[]; + mobileOpen: boolean; + onMobileOpenToggle: () => void; +} + +const NavigationDrawer = ({ items, mobileOpen, onMobileOpenToggle }: NavigationDrawerProps) => { + const theme = useTheme(); + + const iOS = useMemo( + () => typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent), + [], + ); + + const drawer = useMemo( + () => ( + + + + + + + {items.map(item => ( + + ))} + + + ), + [items, onMobileOpenToggle], + ); + + const container = useMemo( + () => (typeof window !== 'undefined' ? window.document.body : undefined), + [], + ); + + return ( + + {drawer} + + ); +}; + +export default NavigationDrawer; diff --git a/website/src/interface.ts b/website/src/interface.ts index 9a54970b..548faa7b 100644 --- a/website/src/interface.ts +++ b/website/src/interface.ts @@ -119,3 +119,21 @@ export interface CommunityData { readonly subtitle: string; readonly sections: CommunityLinksSection[]; } + +export interface MenuLink { + readonly title: string; + readonly url: string; +} + +export interface MenuLinkSubGroup { + readonly title: string; + readonly links: MenuLink[]; +} + +export interface MenuLinkGroup { + readonly title: string; + readonly path: string; + readonly groups: MenuLinkSubGroup[]; +} + +export type MenuItem = MenuLinkGroup | MenuLink; diff --git a/website/src/lib/docs.ts b/website/src/lib/docs.ts index 72c4729e..33913f1f 100644 --- a/website/src/lib/docs.ts +++ b/website/src/lib/docs.ts @@ -6,7 +6,12 @@ import path from 'path'; import { SUMMARY_MIN_PARAGRAPH_LENGTH } from '../constants'; import menu from './menu'; -import type { FileMatter, DocsPage, DocsData, DocsGroup, DocsGroupLink } from '../interface'; +import type { GetStaticProps } from 'next'; +import type { DocsData, DocsGroup, DocsGroupLink, DocsPage, FileMatter } from '../interface'; + +export interface DocsMenuProps { + docsGroups: DocsGroup[]; +} const docsDirectory = path.join(process.cwd(), 'content/docs'); @@ -97,3 +102,11 @@ export function fetchDocsContent(): [DocsPage[], DocsGroup[]] { return docsCache; } + +export const getDocsMenuStaticProps: GetStaticProps = (): { props: DocsMenuProps } => { + return { + props: { + docsGroups: fetchDocsContent()[1], + }, + }; +}; diff --git a/website/src/pages/community.tsx b/website/src/pages/community.tsx index 920eda63..3f13393c 100644 --- a/website/src/pages/community.tsx +++ b/website/src/pages/community.tsx @@ -5,23 +5,44 @@ import CommunitySection from '../components/community/CommunitySection'; import Container from '../components/layout/Container'; import Page from '../components/layout/Page'; import communityData from '../lib/community'; +import { getDocsMenuStaticProps } from '../lib/docs'; -const StyledCommunityContent = styled('div')` - width: 100%; - padding-top: 72px; - min-height: calc(100vh - 72px); - display: flex; - flex-direction: column; - gap: 80px; -`; +import type { DocsMenuProps } from '../lib/docs'; -const StyledTitle = styled('div')` - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 16px; -`; +const StyledCommunityContent = styled('div')( + ({ theme }) => ` + width: 100%; + padding-top: 72px; + min-height: calc(100vh - 72px); + display: flex; + flex-direction: column; + gap: 80px; + + ${theme.breakpoints.between('md', 'lg')} { + padding-top: 48px; + gap: 56px + } + + ${theme.breakpoints.down('md')} { + padding-top: 32px; + gap: 40px + } + `, +); + +const StyledTitle = styled('div')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + + ${theme.breakpoints.down('lg')} { + gap: 8px + } + `, +); const StyledCommunityLinks = styled('section')( ({ theme }) => ` @@ -32,6 +53,14 @@ const StyledCommunityLinks = styled('section')( background: ${theme.palette.mode === 'light' ? '#dddee2' : '#242424'}; padding: 64px 0 32px; flex-grow: 1; + + ${theme.breakpoints.between('md', 'lg')} { + padding: 48px 0 32px; + } + + ${theme.breakpoints.down('md')} { + padding: 40px 0 32px; + } `, ); @@ -43,16 +72,16 @@ const StyledCommunityLinksContent = styled('div')` gap: 40px; `; -const Community = () => { +const Community = ({ docsGroups }: DocsMenuProps) => { return ( - + {communityData.title} - + {communityData.subtitle} @@ -72,3 +101,5 @@ const Community = () => { }; export default Community; + +export const getStaticProps = getDocsMenuStaticProps; diff --git a/website/src/pages/docs/[doc].tsx b/website/src/pages/docs/[doc].tsx index 7a250770..a07981a6 100644 --- a/website/src/pages/docs/[doc].tsx +++ b/website/src/pages/docs/[doc].tsx @@ -26,19 +26,40 @@ const StyledDocsView = styled('div')( padding-top: 16px; ${theme.breakpoints.down('lg')} { - grid-template-columns: unset + margin-left: 0; + padding-top: 24px; + overflow-x: hidden; + width: 100vw; + } + + ${theme.breakpoints.down('md')} { + grid-template-columns: 1fr; } `, ); -const StyledDocsContentWrapper = styled('main')` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - margin: 0; - margin-bottom: 40px; -`; +const StyledDocsContentWrapper = styled('main')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + margin: 0; + margin-bottom: 40px; + + ${theme.breakpoints.between('md', 'lg')} { + width: calc(100vw - 250px); + } + + ${theme.breakpoints.down('lg')} { + margin-bottom: 32px; + } + + ${theme.breakpoints.down('md')} { + width: 100vw; + } + `, +); interface DocsProps { docsGroups: DocsGroup[]; @@ -52,7 +73,13 @@ const Docs = ({ docsGroups, title, slug, description = '', source }: DocsProps) const theme = useTheme(); return ( - + diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 7465fcd6..91c5e109 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -3,29 +3,36 @@ import Card from '@mui/material/Card'; import CardActionArea from '@mui/material/CardActionArea'; import CardContent from '@mui/material/CardContent'; import Chip from '@mui/material/Chip'; -import { styled } from '@mui/material/styles'; +import { styled, useTheme } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import format from 'date-fns/format'; import parseISO from 'date-fns/parseISO'; -import Image from 'next/image'; import Link from 'next/link'; import Container from '../components/layout/Container'; import Page from '../components/layout/Page'; import config from '../lib/config'; +import { getDocsMenuStaticProps } from '../lib/docs'; import homepageData from '../lib/homepage'; import releases from '../lib/releases'; -import type { NextPage } from 'next'; +import type { DocsMenuProps } from '../lib/docs'; -const StyledHomagePageContent = styled('div')` - width: 100%; - padding-top: 72px; - display: flex; - flex-direction: column; - gap: 88px; - align-items: center; -`; +const StyledHomagePageContent = styled('div')( + ({ theme }) => ` + width: 100%; + padding-top: 72px; + display: flex; + flex-direction: column; + gap: 88px; + align-items: center; + + ${theme.breakpoints.down('md')} { + padding-top: 32px; + gap: 0; + } + `, +); const StyledIntroSection = styled('section')` width: 100%; @@ -42,19 +49,32 @@ const StyledIntroSectionContent = styled('div')` align-items: flex-start; `; -const StyledOverviewSection = styled('section')` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; -`; +const StyledOverviewSection = styled('section')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; -const StyledOverviewSectionContent = styled('div')` - width: 100%; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 64px; -`; + ${theme.breakpoints.down('md')} { + margin-top: 64px; + } + `, +); + +const StyledOverviewSectionContent = styled('div')( + ({ theme }) => ` + width: 100%; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 64px; + + ${theme.breakpoints.down('md')} { + grid-template-columns: 1fr; + gap: 24px; + } + `, +); const StyledOverviewList = styled('div')` display: flex; @@ -73,27 +93,61 @@ const StyledImageWrapper = styled('div')` position: relative; `; -const StyledCallToActionSection = styled('section')` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - height: 0; - overflow: visible; - z-index: 1; -`; +const StyledCallToActionSection = styled('section')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + height: 0; + overflow: visible; + z-index: 1; -const StyledCallToActionCard = styled(Card)` - width: 80%; -`; + ${theme.breakpoints.down('md')} { + height: auto; + margin-top: 64px; + } + `, +); -const StyledCallToActionCardContent = styled(CardContent)` - display: flex; - align-items: flex-start; - padding: 24px 40px; - line-height: 30px; - gap: 24px; -`; +const StyledCallToActionContainer = styled('div')( + ({ theme }) => ` + max-width: 1280px; + width: 100%; + padding: 0 40px; + display: flex; + flex-direction: column; + align-items: center; + + ${theme.breakpoints.down('md')} { + padding: 0; + } + `, +); + +const StyledCallToActionCard = styled(Card)( + ({ theme }) => ` + width: 80%; + + ${theme.breakpoints.down('md')} { + width: 100%; + } + `, +); + +const StyledCallToActionCardContent = styled(CardContent)( + ({ theme }) => ` + display: flex; + align-items: flex-start; + padding: 24px 40px; + line-height: 30px; + gap: 24px; + + ${theme.breakpoints.down('md')} { + flex-direction: column; + } + `, +); const StyledCallToActionText = styled('div')` flex-grow: 1; @@ -107,15 +161,25 @@ const StyledReleasesSection = styled('section')( align-items: center; background: ${theme.palette.mode === 'light' ? '#dddee2' : '#242424'}; padding: 64px 0; + + ${theme.breakpoints.down('md')} { + padding: 48px 0; + } `, ); -const StyledReleasesSectionContent = styled('div')` - width: 100%; - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 48px; -`; +const StyledReleasesSectionContent = styled('div')( + ({ theme }) => ` + width: 100%; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 48px; + + ${theme.breakpoints.down('md')} { + grid-template-columns: 1fr; + } + `, +); const StyledReleaseCardContent = styled(CardContent)` width: 100%; @@ -124,29 +188,48 @@ const StyledReleaseCardContent = styled(CardContent)` gap: 8px; `; -const StyledFeaturesSection = styled('section')` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 80px; -`; +const StyledFeaturesSection = styled('section')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding-bottom: 80px; -const StyledFeaturesSectionContent = styled('div')` - width: 100%; - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 48px; -`; + ${theme.breakpoints.down('md')} { + height: auto; + margin-top: 48px; + } + `, +); -const StyledFeaturesSectionIntro = styled('div')` - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - padding: 32px 0 104px; -`; +const StyledFeaturesSectionIntro = styled('div')( + ({ theme }) => ` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 32px 0 104px; + + ${theme.breakpoints.down('md')} { + padding: 32px 0 48px; + } + `, +); + +const StyledFeaturesSectionContent = styled('div')( + ({ theme }) => ` + width: 100%; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 48px; + + ${theme.breakpoints.down('md')} { + grid-template-columns: 1fr; + } + `, +); const StyledFeature = styled('div')` width: 100%; @@ -164,9 +247,11 @@ const StyledFeatureText = styled('div')` padding: 0 16px; `; -const Home: NextPage = () => { +const Home = ({ docsGroups }: DocsMenuProps) => { + const theme = useTheme(); + return ( - + @@ -174,7 +259,7 @@ const Home: NextPage = () => { {homepageData.title} - + {homepageData.subtitle} @@ -191,7 +276,7 @@ const Home: NextPage = () => { {homepageData.overviews.map(overview => ( - + {overview.title} @@ -201,13 +286,14 @@ const Home: NextPage = () => { ))} - + {/* eslint-disable-next-line @next/next/no-img-element */} + - + @@ -231,7 +317,7 @@ const Home: NextPage = () => { - + @@ -272,10 +358,16 @@ const Home: NextPage = () => { {homepageData.features_intro.title} @@ -283,11 +375,15 @@ const Home: NextPage = () => { variant="subtitle1" component="div" color="text.secondary" - sx={{ textAlign: 'center' }} + sx={{ + textAlign: 'center', + [theme.breakpoints.down('md')]: { + textAlign: 'center', + marginTop: '24px', + }, + }} > - {homepageData.features_intro.subtitle1} -
- {homepageData.features_intro.subtitle2} + {homepageData.features_intro.subtitle1} {homepageData.features_intro.subtitle2}
@@ -318,3 +414,5 @@ const Home: NextPage = () => { }; export default Home; + +export const getStaticProps = getDocsMenuStaticProps; diff --git a/website/src/styles/theme.ts b/website/src/styles/theme.ts index da87ce97..44947f1d 100644 --- a/website/src/styles/theme.ts +++ b/website/src/styles/theme.ts @@ -2,71 +2,93 @@ import darkScrollbar from '@mui/material/darkScrollbar'; import { createTheme } from '@mui/material/styles'; import { useMemo } from 'react'; -import type { PaletteMode, ThemeOptions } from '@mui/material'; - -const commonThemeProps: ThemeOptions = {}; +import type { Components, PaletteMode, PaletteOptions, Theme } from '@mui/material'; const useCreateTheme = (mode: PaletteMode) => { - return useMemo( + const theme = useMemo(() => createTheme(), []); + + const palette: PaletteOptions = useMemo( () => mode === 'light' - ? createTheme({ - ...commonThemeProps, - palette: { - mode, - primary: { - main: '#3764be', - }, - secondary: { - main: '#3764be', - }, - background: { - default: '#f9f9f9', - paper: '#f9f9f9', - }, + ? { + mode, + primary: { + main: '#3764be', }, - typography: { - fontFamily: - "'Roboto', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif", - h1: { - fontSize: '42px', - fontWeight: 'bold', - }, + secondary: { + main: '#3764be', }, - }) - : createTheme({ - ...commonThemeProps, - components: { - MuiCssBaseline: { - styleOverrides: { - body: darkScrollbar(), - }, - }, + background: { + default: '#f9f9f9', + paper: '#f9f9f9', }, - palette: { - mode, - primary: { - main: '#3A69C7', - }, - secondary: { - main: '#5ecffb', - }, - background: { - default: '#2e3034', - paper: '#2e3034', - }, + } + : { + mode, + primary: { + main: '#3A69C7', }, - typography: { - fontFamily: - "'Roboto', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif", - h1: { - fontSize: '42px', - fontWeight: 'bold', - }, + secondary: { + main: '#5ecffb', }, - }), + background: { + default: '#2e3034', + paper: '#2e3034', + }, + }, [mode], ); + + const components: Components> | undefined = useMemo( + () => + mode === 'light' + ? {} + : { + MuiCssBaseline: { + styleOverrides: { + body: darkScrollbar(), + }, + }, + }, + [mode], + ); + + return useMemo( + () => + createTheme({ + palette, + typography: { + fontFamily: "'Roboto', -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif", + h1: { + fontSize: '42px', + fontWeight: 'bold', + lineHeight: 1.3, + marginBottom: '16px', + [theme.breakpoints.down('md')]: { + fontSize: '30px', + }, + }, + h2: { + fontSize: '24px', + lineHeight: 1.3, + position: 'relative', + [theme.breakpoints.down('md')]: { + fontSize: '20px', + }, + }, + h3: { + fontSize: '20px', + lineHeight: 1.3, + position: 'relative', + [theme.breakpoints.down('md')]: { + fontSize: '18px', + }, + }, + }, + components, + }), + [components, palette, theme.breakpoints], + ); }; export default useCreateTheme;