From e16851ab1c1ad8aa93e7475c03115b675e53f7cb Mon Sep 17 00:00:00 2001 From: "Josh Berman [SSW]" <137844305+joshbermanssw@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:51:10 +1100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=8F=20Move=20docs=20from=20pages=20to?= =?UTF-8?q?=20app=20router=20(#2712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move docs from pages to app router * fix import * fix more imports * fix products page * add hover to navy * added TODO --- app/docs/[...slug]/DocsPagesClient.tsx | 113 ++++++ app/docs/[...slug]/page.tsx | 74 ++++ app/docs/page.tsx | 37 ++ app/tina-client.tsx | 25 ++ app/whats-new/tinacms/page.tsx | 3 +- .../DocsNavigationList.tsx | 294 +++++++++++++++ .../DocumentationNavigation/Breadcrumbs.tsx | 58 +++ .../DocumentationNavigation/DocsHeaderNav.tsx | 41 +++ .../DocsLeftSidebar.tsx | 68 ++++ .../DocsNavigationList.tsx | 285 +++++++++++++++ .../DocumentationNavigation.tsx | 124 +++++++ .../DocumentationNavigation/VersionSelect.tsx | 137 +++++++ .../Docs/DocumentationNavigation/index.tsx | 1 + .../Docs/docsMain/directoryOverflowButton.tsx | 50 +++ .../Docs/docsMain/docsMainBody.tsx | 22 ++ .../Docs/docsMain/docsMobileHeader.tsx | 77 ++++ .../Docs/docsMain/tocOverflowButton.tsx | 73 ++++ .../Docs/docsSearch/SearchComponent.tsx | 194 ++++++++++ .../Docs/docsSearch/SearchNavigation.tsx | 281 +++++++++++++++ .../Docs/toc/index.tsx | 194 ++++++++++ .../Docs/toc/toc-item.template.tsx | 15 + .../Docs/toc/toc-submenu.template.tsx | 36 ++ .../Docs/toc_helper.ts | 66 ++++ .../DocsNavigationList.tsx | 1 - components/blocks/CardGrid.tsx | 113 ++---- components/docsMain/docsMainBody.tsx | 1 - components/hooks/ScreenResizer.tsx | 21 ++ components/layout/Footer.tsx | 3 - .../docAndBlogComponents.tsx | 2 +- .../scrollBasedShowcase.tsx | 336 ++++++------------ components/toc/index.tsx | 1 + pages/docs/[...slug].tsx | 321 ----------------- pages/docs/index.tsx | 12 - 33 files changed, 2425 insertions(+), 654 deletions(-) create mode 100644 app/docs/[...slug]/DocsPagesClient.tsx create mode 100644 app/docs/[...slug]/page.tsx create mode 100644 app/docs/page.tsx create mode 100644 app/tina-client.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation copy/DocsNavigationList.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/Breadcrumbs.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsHeaderNav.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsLeftSidebar.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsNavigationList.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocumentationNavigation.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/VersionSelect.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/DocumentationNavigation/index.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsMain/directoryOverflowButton.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsMain/docsMainBody.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsMain/docsMobileHeader.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsMain/tocOverflowButton.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsSearch/SearchComponent.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/docsSearch/SearchNavigation.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/toc/index.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/toc/toc-item.template.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/toc/toc-submenu.template.tsx create mode 100644 components/AppRouterMigrationComponents/Docs/toc_helper.ts create mode 100644 components/hooks/ScreenResizer.tsx delete mode 100644 pages/docs/[...slug].tsx delete mode 100644 pages/docs/index.tsx diff --git a/app/docs/[...slug]/DocsPagesClient.tsx b/app/docs/[...slug]/DocsPagesClient.tsx new file mode 100644 index 0000000000..642550a881 --- /dev/null +++ b/app/docs/[...slug]/DocsPagesClient.tsx @@ -0,0 +1,113 @@ +'use client'; + +import MainDocsBodyHeader from 'components/AppRouterMigrationComponents/Docs/docsMain/docsMainBody'; +import TocOverflowButton from 'components/AppRouterMigrationComponents/Docs/docsMain/tocOverflowButton'; +import { LeftHandSideParentContainer } from 'components/AppRouterMigrationComponents/Docs/docsSearch/SearchNavigation'; +import ToC from 'components/AppRouterMigrationComponents/Docs/toc'; +import { screenResizer } from 'components/hooks/ScreenResizer'; +import { docAndBlogComponents } from 'components/tinaMarkdownComponents/docAndBlogComponents'; +import { DocsPagination } from 'components/ui'; +import { TinaMarkdown } from 'tinacms/dist/rich-text'; +import { useTocListener } from 'components/AppRouterMigrationComponents/Docs/toc_helper'; + +export default function DocsClient(props) { + + const { PageTableOfContents, NavigationDocsData } = props.props; + const DocumentationData = props.tinaProps.data.doc; + + const allData = [DocumentationData, PageTableOfContents, NavigationDocsData]; + + + const isScreenSmallerThan1200 = screenResizer().isScreenSmallerThan1200; + const isScreenSmallerThan840 = screenResizer().isScreenSmallerThan840; + const { activeIds, contentRef } = useTocListener(DocumentationData); + + const previousPage = { + slug: DocumentationData?.previous?.id.slice(7, -4), + title: DocumentationData?.previous?.title, + }; + + const nextPage = { + slug: DocumentationData?.next?.id.slice(7, -4), + title: DocumentationData?.next?.title, + }; + + + + const lastEdited = DocumentationData?.last_edited; + const date = lastEdited === null ? null : new Date(lastEdited); + const formattedDate = date + ? date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : ''; + const gridClass = isScreenSmallerThan840 + ? 'grid-cols-1' + : isScreenSmallerThan1200 + ? 'grid-cols-[1.25fr_3fr]' + : 'grid-cols-[1.25fr_3fr_0.75fr]'; + + + + return ( +
+
+ {/* LEFT COLUMN */} +
+ +
+ {/* MIDDLE COLUMN */} +
+ + {isScreenSmallerThan1200 && !DocumentationData?.tocIsHidden && ( + + )} +
+ {' '} + +
+ + {formattedDate && ( + + {' '} + Last Edited: {formattedDate} + + )} + +
+ {/* RIGHT COLUMN */} + {DocumentationData?.tocIsHidden ? null : ( +
+ +
+ )} +
+
+ ); +} diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx new file mode 100644 index 0000000000..de8769dbc4 --- /dev/null +++ b/app/docs/[...slug]/page.tsx @@ -0,0 +1,74 @@ +import { docAndBlogComponents } from 'components/tinaMarkdownComponents/docAndBlogComponents'; +import { glob } from 'fast-glob'; +import { NextSeo } from 'next-seo'; +import { notFound } from 'next/navigation'; +import client from 'tina/__generated__/client'; +import { TinaMarkdown } from 'tinacms/dist/rich-text'; +import { getDocsNav } from 'utils/docs/getDocProps'; +import getTableOfContents from 'utils/docs/getTableOfContents'; +import { TinaClient } from 'app/tina-client'; +import DocsClient from './DocsPagesClient'; + +export async function generateStaticParams() { + const contentDir = './content/docs/'; + const files = await glob(`${contentDir}**/*.mdx`); + + return files + .filter((file) => !file.endsWith('index.mdx')) + .map((file) => { + const path = file.substring(contentDir.length, file.length - 4); // Remove "./content/docs/" and ".mdx" + return { slug: path.split('/') }; + }); +} + +export async function generateMetadata({ + params, +}: { + params: { slug: string[] }; +}) { + const slug = params.slug.join('/'); + const { data } = await client.queries.doc({ relativePath: `${slug}.mdx` }); + + return { + title: `${data.doc.seo?.title || data.doc.title} | TinaCMS Docs`, + description: data.doc.seo?.description || '', + openGraph: { + title: data.doc.title, + description: data.doc.seo?.description, + }, + }; +} + +export default async function DocPage({ + params, +}: { + params: { slug: string[] }; +}) { + const slug = params.slug.join('/'); + + try { + const [results, navDocData] = await Promise.all([ + client.queries.doc({ relativePath: `${slug}.mdx` }), + getDocsNav(), + ]); + + const docData = results.data.doc; + const PageTableOfContents = getTableOfContents(docData.body.children); + + return ( + + ); + } catch (error) { + notFound(); + } +} diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000000..ed568ed90f --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,37 @@ +import { notFound } from 'next/navigation'; +import client from 'tina/__generated__/client'; +import { TinaClient } from 'app/tina-client'; +import DocsClient from './[...slug]/DocsPagesClient'; +import { getDocsNav } from 'utils/docs/getDocProps'; +import getTableOfContents from 'utils/docs/getTableOfContents'; + +export default async function DocsPage() { + const slug = 'index'; // Default root document slug for /docs + + try { + const [results, navDocData] = await Promise.all([ + client.queries.doc({ relativePath: `${slug}.mdx` }), + getDocsNav(), + ]); + + const docData = results.data.doc; + const PageTableOfContents = getTableOfContents(docData.body.children); + + return ( + + ); + } catch (error) { + + notFound(); + } +} diff --git a/app/tina-client.tsx b/app/tina-client.tsx new file mode 100644 index 0000000000..559450f4d8 --- /dev/null +++ b/app/tina-client.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { useTina } from 'tinacms/dist/react'; +import React from 'react'; + +export type UseTinaProps = { + query: string; + variables: object; + data: object; +}; + +export type TinaClientProps = { + props: UseTinaProps & T; + Component: React.FC<{ tinaProps: { data: object }; props: T }>; +}; + +export function TinaClient({ props, Component }: TinaClientProps) { + const { data } = useTina({ + query: props.query, + variables: props.variables, + data: props.data, + }); + + return ; +} diff --git a/app/whats-new/tinacms/page.tsx b/app/whats-new/tinacms/page.tsx index 7c5cfb2f2d..74302d7d5d 100644 --- a/app/whats-new/tinacms/page.tsx +++ b/app/whats-new/tinacms/page.tsx @@ -38,8 +38,7 @@ export default async function TinaCMSPage() { const { data, query } = await fetchWhatsNewData(vars); - console.log(data); - // return
banana
+ return ; } diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation copy/DocsNavigationList.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation copy/DocsNavigationList.tsx new file mode 100644 index 0000000000..e6faf01d0c --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation copy/DocsNavigationList.tsx @@ -0,0 +1,294 @@ +import React, { createContext } from 'react'; +import styled, { css } from 'styled-components'; +import { DocsNavProps } from 'components/DocumentationNavigation'; +import { useRouter } from 'next/router'; +import { matchActualTarget } from 'utils'; +import { DynamicLink } from '../../../../components/ui'; +import docsLinks from '../../../../content/docs-navigation.json'; +import { BiChevronRight } from 'react-icons/bi'; +import AnimateHeight from 'react-animate-height'; +import data from '../../../../content/siteConfig.json'; + +interface NavTitleProps { + level: number; + selected: boolean; + childSelected?: boolean; + children: React.ReactNode | React.ReactNode[]; + onClick?: () => void; +} + + + +const NavTitle = ({ + children, + level = 3, + selected, + childSelected, + ...props +}: NavTitleProps) => { + const headerLevelClasses = { + 0: 'opacity-100 font-tuner-light text-orange-500 text-xl pt-2', + 1: { + default: 'text-base font-sans pt-1 text-gray-800', + selected: 'text-base font-sans pt-1 font-bold text-blue-500', + childSelected: 'text-base font-sans pt-1 font-[500] text-gray-800', + }, + 2: { + default: 'text-[15px] font-sans opacity-80 pt-0.5 text-gray-700', + selected: 'text-[15px] font-sans pt-0.5 font-bold text-blue-500', + childSelected: 'text-[15px] font-sans pt-1 font-[500] text-gray-800', + }, + 3: { + default: 'text-[15px] font-sans opacity-80 pt-0.5 text-gray-700', + selected: 'text-[15px] font-sans pt-0.5 font-bold text-blue-500', + childSelected: 'text-[15px] font-sans pt-1 font-[500] text-gray-800', + }, + }; + + const headerLevel = level > 3 ? 3 : level; + const selectedClass = selected + ? 'selected' + : childSelected + ? 'childSelected' + : 'default'; + const classes = + level < 1 + ? headerLevelClasses[headerLevel] + : headerLevelClasses[headerLevel][selectedClass]; + + return ( +
+ {children} +
+ ); +}; + +const hasNestedSlug = (navItems = [], slug) => { + for (let item of navItems) { + if (matchActualTarget(item.slug || item.href, slug)) { + return true; + } + if (item.items) { + if (hasNestedSlug(item.items, slug)) { + return true; + } + } + } + return false; +}; + +const NavLevel = ({ + navListElem, + categoryData, + level = 0, +}: { + navListElem?: any; + categoryData: any; + level?: number; +}) => { + const navLevelElem = React.useRef(null); + const router = useRouter(); + const path = router.asPath; + const slug = categoryData.slug?.replace(/\/$/, ''); + const [expanded, setExpanded] = React.useState( + matchActualTarget(slug || categoryData.href, path) || + hasNestedSlug(categoryData.items, path) || + level === 0 + ); + + + const selected = + path.split('#')[0] == slug || (slug == '/docs' && path == '/docs/'); + + const childSelected = hasNestedSlug(categoryData.items, router.asPath); + React.useEffect(() => { + if ( + navListElem && + navLevelElem.current && + navListElem.current && + selected + ) { + const scrollOffset = navListElem.current.scrollTop; + const navListOffset = navListElem.current.getBoundingClientRect().top; + const navListHeight = navListElem.current.offsetHeight; + const navItemOffset = navLevelElem.current.getBoundingClientRect().top; + const elementOutOfView = + navItemOffset - navListOffset > navListHeight + scrollOffset; + + if (elementOutOfView) { + navLevelElem.current.scrollIntoView({ + behavior: 'auto', + block: 'center', + inline: 'nearest', + }); + } + } + }, [navLevelElem.current, navListElem, selected]); + return ( + <> + + {categoryData.slug ? ( + + + {categoryData.title} + + + ) : ( + { + setExpanded(!expanded); + }} + > + {categoryData.title} + {categoryData.items && !selected && ( + + )} + + )} + + {!childSelected && selected && level > 0 && ( +
+ )} +
+ {categoryData.items && ( + + + {(categoryData.items || []).map((item) => ( +
+ +
+ ))} +
+
+ )} + + ); +}; + +interface NavLevelChildContainerProps { + level: number; +} + +const NavLevelChildContainer = styled.div` + position: relative; + display: block; + padding-left: 0.675rem; + padding-top: 0.125rem; + padding-bottom: 0.125rem; + + ${(props: any) => + props.level === 0 && + css` + padding-left: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.125rem; + `} +`; + +const NavLabelContainer = styled.div<{ status: string }>` + position: relative; + display: flex; + + &:last-child { + margin-bottom: 0.375rem; + } + + ${(props: { status: string }) => + props.status && + css` + a::after { + display: -ms-inline-flexbox; + content: '${props.status.toLowerCase()}'; + text-transform: capitalize; + font-size: 12px; + font-weight: bold; + background-color: #f9ebe6; + border: 1px solid #edcdc4; + width: fit-content; + padding: 2px 5px; + border-radius: 5px; + letter-spacing: 0.25px; + color: #ec4815; + margin-right: 5px; + margin-left: 5px; + line-height: 1; + vertical-align: middle; + height: fit-content; + align-self: center; + } + `} +`; + +export const DocsNavigationList = ({ navItems }: DocsNavProps) => { + const navListElem = React.useRef(null); + + return ( + <> + + {navItems?.map((categoryData) => ( + + ))} + + + ); +}; + +const DocsNavigationContainer = styled.div` + overflow-y: auto; + overflow-x: hidden; + padding: 0.5rem 0 1.5rem 0; + margin-right: -1px; + + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + background-color: transparent; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.3); + border-radius: 4px; + } + + @media (min-width: 1600px) { + padding: 1rem 1rem 2rem 1rem; + } +`; + +const AnchorIcon = styled.span` + display: inline-block; + position: relative; + transform: translate3d(0, 0, 0); + transition: all 150ms ease-out; +`; diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/Breadcrumbs.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/Breadcrumbs.tsx new file mode 100644 index 0000000000..cb9c4005de --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/Breadcrumbs.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import React from 'react'; +import { FaChevronRight } from 'react-icons/fa'; +import { matchActualTarget } from 'utils'; + +export interface DocsNavProps { + navItems: any; +} + +const getNestedBreadcrumbs = ( + listItems: any[], + pagePath: string, + breadcrumbs: any[] = [] +) => { + for (const listItem of listItems || []) { + if (matchActualTarget(pagePath, listItem.slug || listItem.href)) { + breadcrumbs.push(listItem); + return [listItem]; + } + const nestedBreadcrumbs = getNestedBreadcrumbs( + listItem.items, + pagePath, + breadcrumbs + ); + if (nestedBreadcrumbs.length) { + return [listItem, ...nestedBreadcrumbs]; + } + } + return []; +}; + +export function Breadcrumbs({ navItems }: DocsNavProps) { + const pathname = usePathname(); + const breadcrumbs = getNestedBreadcrumbs(navItems, pathname) || []; + + return ( + + ); +} diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsHeaderNav.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsHeaderNav.tsx new file mode 100644 index 0000000000..5e4ca4c34f --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsHeaderNav.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import data from '../../../../content/docs-navigation.json' +import { LinkButton } from '../../../ui' +import { DynamicLink } from '../../../ui/DynamicLink' + +export const DocsHeaderNav = () => { + return ( + + ) +} diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsLeftSidebar.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsLeftSidebar.tsx new file mode 100644 index 0000000000..a8496ce75e --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsLeftSidebar.tsx @@ -0,0 +1,68 @@ +import styled, { css } from 'styled-components' + +export const DocsLeftSidebar = styled.div<{ open: boolean }>` + line-height: 1.25; + background-color: rgba(255, 255, 255, 0.5); + + padding: 10px; + position: fixed; + z-index: 39; + left: 0; + top: 0; + width: 80%; + min-width: 16rem; + max-width: 24rem; + height: 100%; + transform: translate3d(-100%, 0, 0); + transition: all 140ms ease-in; + display: flex; + flex-direction: column; + align-content: space-between; + overflow: visible; + + border-radius: 0.75rem; + box-shadow: 0 15px 20px -5px rgba(0, 0, 0, 0.05), + 0 -5px 30px -5px rgba(0, 0, 0, 0.05), + 0 15px 20px -5px rgba(0, 0, 0, 0.05), + 0 -5px 20px -5px rgba(0, 0, 0, 0.05); + + > ul { + flex: 1 1 auto; + padding: 1rem 1px 1rem 0; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0) 1rem), + linear-gradient(to bottom, var(--color-grey-1), white 1rem); + background-attachment: local, scroll; + background-repeat: no-repeat; + background-size: 100% 1rem, 100% 1rem; + } + + ${props => + props.open + ? css` + transition: all 240ms ease-out; + transform: translate3d(0, 0, 0); + ` + : ``}; + + @media (min-width: 840px) { + position: sticky; + height: calc(100vh - 80px); + top: 40px; + grid-area: sidebar; + place-self: stretch; + width: 100%; + left: 40px; + transform: translate3d(0, 0, 0); + margin-top: 30px; + margin-bottom: 40px; + + + } + + @media (max-width: 840px) { + background-color: var(--color-light); + height: 100%; + padding: 0; + + } +` diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsNavigationList.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsNavigationList.tsx new file mode 100644 index 0000000000..67ff43da2f --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocsNavigationList.tsx @@ -0,0 +1,285 @@ +'use client'; + +import React, { createContext } from 'react'; +import styled, { css } from 'styled-components'; +import { DocsNavProps } from './DocumentationNavigation'; +import { usePathname } from 'next/navigation'; +import { matchActualTarget } from 'utils'; +import { DynamicLink } from 'components/ui'; +import docsLinks from '../../../../content/docs-navigation.json'; +import { BiChevronRight } from 'react-icons/bi'; +import AnimateHeight from 'react-animate-height'; +import data from '../../../../content/siteConfig.json'; + +interface NavTitleProps { + level: number; + selected: boolean; + childSelected?: boolean; + children: React.ReactNode | React.ReactNode[]; + onClick?: () => void; +} + +const NavTitle = ({ + children, + level = 3, + selected, + childSelected, + ...props +}: NavTitleProps) => { + const headerLevelClasses = { + 0: 'opacity-100 font-tuner-light text-orange-500 text-xl pt-2', + 1: { + default: 'text-base font-sans pt-1 text-gray-800', + selected: 'text-base font-sans pt-1 font-bold text-blue-500', + childSelected: 'text-base font-sans pt-1 font-[500] text-gray-800', + }, + 2: { + default: 'text-[15px] font-sans opacity-80 pt-0.5 text-gray-700', + selected: 'text-[15px] font-sans pt-0.5 font-bold text-blue-500', + childSelected: 'text-[15px] font-sans pt-1 font-[500] text-gray-800', + }, + 3: { + default: 'text-[15px] font-sans opacity-80 pt-0.5 text-gray-700', + selected: 'text-[15px] font-sans pt-0.5 font-bold text-blue-500', + childSelected: 'text-[15px] font-sans pt-1 font-[500] text-gray-800', + }, + }; + + const headerLevel = level > 3 ? 3 : level; + const selectedClass = selected + ? 'selected' + : childSelected + ? 'childSelected' + : 'default'; + const classes = + level < 1 + ? headerLevelClasses[headerLevel] + : headerLevelClasses[headerLevel][selectedClass]; + + return ( +
+ {children} +
+ ); +}; + +const hasNestedSlug = (navItems = [], slug) => { + for (let item of navItems) { + if (matchActualTarget(item.slug || item.href, slug)) { + return true; + } + if (item.items) { + if (hasNestedSlug(item.items, slug)) { + return true; + } + } + } + return false; +}; + +const NavLevel = ({ + navListElem, + categoryData, + level = 0, +}: { + navListElem?: any; + categoryData: any; + level?: number; +}) => { + const navLevelElem = React.useRef(null); + const pathname = usePathname(); // Replace useRouter with usePathname + const path = pathname || ''; // Get current path + const slug = categoryData.slug?.replace(/\/$/, ''); + const [expanded, setExpanded] = React.useState( + matchActualTarget(slug || categoryData.href, path) || + hasNestedSlug(categoryData.items, path) || + level === 0 + ); + + const selected = + path.split('#')[0] === slug || (slug === '/docs' && path === '/docs/'); + + const childSelected = hasNestedSlug(categoryData.items, path); + + React.useEffect(() => { + if ( + navListElem && + navLevelElem.current && + navListElem.current && + selected + ) { + const scrollOffset = navListElem.current.scrollTop; + const navListOffset = navListElem.current.getBoundingClientRect().top; + const navListHeight = navListElem.current.offsetHeight; + const navItemOffset = navLevelElem.current.getBoundingClientRect().top; + const elementOutOfView = + navItemOffset - navListOffset > navListHeight + scrollOffset; + + if (elementOutOfView) { + navLevelElem.current.scrollIntoView({ + behavior: 'auto', + block: 'center', + inline: 'nearest', + }); + } + } + }, [navLevelElem.current, navListElem, selected]); + + return ( + <> + + {categoryData.slug ? ( + + + {categoryData.title} + + + ) : ( + { + setExpanded(!expanded); + }} + > + {categoryData.title} + {categoryData.items && !selected && ( + + )} + + )} + + {categoryData.items && ( + + + {(categoryData.items || []).map((item) => ( +
+ +
+ ))} +
+
+ )} + + ); +}; + +interface NavLevelChildContainerProps { + level: number; +} + +const NavLevelChildContainer = styled.div` + position: relative; + display: block; + padding-left: 0.675rem; + padding-top: 0.125rem; + padding-bottom: 0.125rem; + + ${(props: any) => + props.level === 0 && + css` + padding-left: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.125rem; + `} +`; + +const NavLabelContainer = styled.div<{ status: string }>` + position: relative; + display: flex; + + &:last-child { + margin-bottom: 0.375rem; + } + + ${(props: { status: string }) => + props.status && + css` + a::after { + display: -ms-inline-flexbox; + content: '${props.status.toLowerCase()}'; + text-transform: capitalize; + font-size: 12px; + font-weight: bold; + background-color: #f9ebe6; + border: 1px solid #edcdc4; + width: fit-content; + padding: 2px 5px; + border-radius: 5px; + letter-spacing: 0.25px; + color: #ec4815; + margin-right: 5px; + margin-left: 5px; + line-height: 1; + vertical-align: middle; + height: fit-content; + align-self: center; + } + `} +`; + +export const DocsNavigationList = ({ navItems }: DocsNavProps) => { + const navListElem = React.useRef(null); + + return ( + + {navItems?.map((categoryData) => ( + + ))} + + ); +}; + +const DocsNavigationContainer = styled.div` + overflow-y: auto; + overflow-x: hidden; + padding: 0.5rem 0 1.5rem 0; + margin-right: -1px; + + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + background-color: transparent; + } + + ::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.3); + border-radius: 4px; + } + + @media (min-width: 1600px) { + padding: 1rem 1rem 2rem 1rem; + } +`; + +const AnchorIcon = styled.span` + display: inline-block; + position: relative; + transform: translate3d(0, 0, 0); + transition: all 150ms ease-out; +`; diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocumentationNavigation.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocumentationNavigation.tsx new file mode 100644 index 0000000000..92498b72a1 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/DocumentationNavigation.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { Overlay } from '../../../ui/Overlay'; +import { DocsLeftSidebar } from './DocsLeftSidebar'; +import { DocsNavigationList } from './DocsNavigationList'; +import styled from 'styled-components'; +import { useRouter } from 'next/router'; +import { FallbackPlaceholder } from '../../../../components/fallback-placeholder'; +import Search from '../../../search'; +import { HitsWrapper } from '../../../../components/search/styles'; +import { searchIndices } from '../../../../components/search/indices'; +import { VersionSelect } from './VersionSelect'; +import { BiMenu } from 'react-icons/bi'; +import { IoMdClose } from 'react-icons/io'; +import { LeftHandSideParentContainer } from 'components/docsSearch/SearchNavigation'; + +export interface DocsNavProps { + navItems: any; +} + +export function DocumentationNavigation({ navItems }: DocsNavProps) { + const [mobileNavIsOpen, setMobileNavIsOpen] = useState(false); + const router = useRouter(); + return ( + <> + + {!mobileNavIsOpen && ( + setMobileNavIsOpen(!mobileNavIsOpen)} + /> + )} + + setMobileNavIsOpen(false)} + /> + + ); +} + + +const MobileNavToggle = ({ open, onClick }: { open: boolean, onClick: () => void }) => { + return ( + + + + ); +}; + + +const CloseButton = styled.button<{ mobileNavIsOpen: boolean }>` + position: absolute; + top: 20px; + right: 20px; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + + .icon { + font-size: 1.75rem; + transition: transform 0.3s ease; + } + + @media (min-width: 840px) { + display: none; + } + + display: ${(props) => (props.mobileNavIsOpen ? 'block' : 'none')}; +`; + +const ToggleWrapper = styled.button<{ open: boolean }>` + position: fixed; + top: 20px; + left: ${(props) => (props.open ? 'auto' : '0px')}; + right: 20px; + background: var(--color-light); + padding: 0 0 0 1rem; + border-radius: 0 2rem 2rem 0; + width: 3.25rem; + z-index: 49; + transition: left 0.3s ease, right 0.3s ease; + display: flex; + justify-content: center; + align-items: center; + + .icon { + font-size: 1.5rem; + transition: transform 0.3s ease; + } + + .menu-icon { + transform: rotate(0deg); + } + + @media (min-width: 840px) { + display: none; + } +`; + +const DocsSidebarHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +`; + +const DocsSidebarHeaderWrapper = styled.div` + flex: 0 0 auto; + background-color: transparent; + z-index: 500; + padding: 1rem 1rem 1.25rem 1rem; + position: relative; + + ${HitsWrapper} { + right: auto; + left: 1.25rem; + margin-top: -1.625rem; + } + + @media (min-width: 1600px) { + padding: 1rem 1.75rem 1.5rem 1.75rem; + } +`; diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/VersionSelect.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/VersionSelect.tsx new file mode 100644 index 0000000000..ff7b8a643c --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/VersionSelect.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react'; +import { FaChevronDown } from 'react-icons/fa'; +import styled from 'styled-components'; + +const VERSIONS = [ + { + id: 'v-latest', + url: 'https://tina.io', + label: 'v.Latest', + }, + { + id: 'v-0.68.13', + url: 'https://tinacms-site-next-i08bcbicy-tinacms.vercel.app/', + label: 'v.0.68.13', + }, + { + id: 'v-0.67.3', + url: 'https://tinacms-site-next-pu1t2v9y4-tinacms.vercel.app', + label: 'v.0.67.3', + }, + { + id: 'v-pre-beta', + url: 'https://pre-beta.tina.io', + label: 'v.Pre-Beta', + }, +]; + +export const VersionSelect = () => { + const [isOpen, setIsOpen] = useState(false); + const [selectedVersion, setSelectedVersion] = useState( + VERSIONS.find( + (v) => typeof window !== 'undefined' && v.url === window.location.origin + ) || VERSIONS[0] + ); + + return ( + + + + + + {isOpen && ( + + {VERSIONS.map((version) => ( +
  • + +
  • + ))} +
    + )} +
    + ); +}; + +const SelectWrapper = styled.div` + display: flex; + justify-content: flex-end + flex-grow: 1; + position: relative; + + @media (min-width: 840px) { + display: inline-block; + flex-grow: 0; + } + +.dropdown-button { + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + background-color: white; + border: 1px solid var(--color-grey-1); + color: var(--color-grey-7); + display: flex; + align-items: center; + border-radius: 100px; + transition: filter 250ms ease; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + position: relative; + background-repeat: no-repeat; + background-position: right 0.7em top 50%; + background-size: 0.65em auto; + cursor: pointer; +} + +`; + +const DropdownList = styled.ul` + position: absolute; + width: 100%; + background-color: white; + border: 1px solid var(--color-grey-1); + border-radius: 0.375rem; + padding: 0.5rem 0; + z-index: 10; + + li { + list-style: none; + + .option { + width: 100%; + padding: 0.5rem 0.75rem; + background-color: white; + text-align: left; + color: var(--color-grey-7); + cursor: pointer; + transition: background-color 250ms ease; + + &:hover { + background-color: var(--color-grey-2); + } + + &.selected { + color: red; + } + } + } +`; diff --git a/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/index.tsx b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/index.tsx new file mode 100644 index 0000000000..ac781b2f53 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/DocumentationNavigation/index.tsx @@ -0,0 +1 @@ +export * from './DocumentationNavigation' diff --git a/components/AppRouterMigrationComponents/Docs/docsMain/directoryOverflowButton.tsx b/components/AppRouterMigrationComponents/Docs/docsMain/directoryOverflowButton.tsx new file mode 100644 index 0000000000..890207fcf1 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsMain/directoryOverflowButton.tsx @@ -0,0 +1,50 @@ +import { DocsNavigationList } from 'components/DocumentationNavigation/DocsNavigationList'; +import { useState, useEffect, useRef } from 'react'; +import { MdMenu } from 'react-icons/md'; + +const DirectoryOverflow = ({ tocData }) => { + return ( +
    + +
    + ); +}; + +const DirectoryOverflowButton = (tocData) => { + const [isTableOfContentsOpen, setIsTableOfContentsOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (containerRef.current && !containerRef.current.contains(event.target)) { + setIsTableOfContentsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
    +
    setIsTableOfContentsOpen(!isTableOfContentsOpen)} + > + + + Topics + +
    + {isTableOfContentsOpen && ( +
    + +
    + )} +
    + ); +}; + +export default DirectoryOverflowButton; diff --git a/components/AppRouterMigrationComponents/Docs/docsMain/docsMainBody.tsx b/components/AppRouterMigrationComponents/Docs/docsMain/docsMainBody.tsx new file mode 100644 index 0000000000..f89e70dce0 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsMain/docsMainBody.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { Breadcrumbs } from '../DocumentationNavigation/Breadcrumbs'; +import DocsMobileHeader from './docsMobileHeader'; + +const MainDocsBodyHeader = ({ DocumentTitle, NavigationDocsItems, allData, screenResizing }) => { + return ( +
    + {/* Uncomment these lines if needed */} + {screenResizing && ( + + )} + + +
    + {DocumentTitle} +
    +
    + ); +}; + +export default MainDocsBodyHeader; diff --git a/components/AppRouterMigrationComponents/Docs/docsMain/docsMobileHeader.tsx b/components/AppRouterMigrationComponents/Docs/docsMain/docsMobileHeader.tsx new file mode 100644 index 0000000000..82184e48d6 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsMain/docsMobileHeader.tsx @@ -0,0 +1,77 @@ +import { DocsSearchBarHeader } from 'components/docsSearch/SearchNavigation'; +import { useState } from 'react'; +import { FaChevronRight } from 'react-icons/fa'; +import DirectoryOverflowButton from './directoryOverflowButton'; + +export const MobileVersionSelect = () => { + const versions = [ + ['v.Latest', ''], + [ + 'v.0.68.13', + 'https://tinacms-site-next-i08bcbicy-tinacms.vercel.app/docs/', + ], + ['v.0.67.3', 'https://tinacms-site-next-pu1t2v9y4-tinacms.vercel.app/'], + ['v.Pre-Beta', 'https://pre-beta.tina.io/'], + ]; + const [versionSelected, setVersionSelected] = useState(versions[0][0]); + const [isOverflowOpen, setIsOverflowOpen] = useState(false); + + const handleVersionClick = (version) => { + setVersionSelected(version[0]); + setIsOverflowOpen(false); + + if (version[0] !== 'v.Latest') { + window.location.href = version[1]; + } + }; + + return ( +
    + {/* VERSION SELECT PILL BUTTON */} +
    setIsOverflowOpen(!isOverflowOpen)} + > +
    {versionSelected}
    +
    + +
    +
    + {/* VERSION SELECT OVERFLOW */} + {isOverflowOpen && ( +
    + {versions.map((version, index) => ( +
    handleVersionClick(version)} + > + {version[0]} +
    + ))} +
    + )} +
    + ); +}; + +const DocsMobileHeader = (data) => { + return ( +
    + + +
    + ); +}; + +export default DocsMobileHeader; diff --git a/components/AppRouterMigrationComponents/Docs/docsMain/tocOverflowButton.tsx b/components/AppRouterMigrationComponents/Docs/docsMain/tocOverflowButton.tsx new file mode 100644 index 0000000000..cf43d1b217 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsMain/tocOverflowButton.tsx @@ -0,0 +1,73 @@ +import Link from 'next/link'; +import { useState, useEffect, useRef } from 'react'; +import { MdMenu } from 'react-icons/md'; +import { getDocId } from 'utils/docs/getDocIds'; + +const TocOverflow = ({ tocData }) => { + return ( +
    + {tocData.tocData.map((item, index) => { + const textIndentation = + item.type === 'h3' ? 'ml-4' : item.type === 'h4' ? 'ml-8' : ''; + + const linkHref = `#${item.text.replace(/\s+/g, '-').toLowerCase()}`; + + return ( + + {item.text} + + ); + })} +
    + ); +}; + +const TocOverflowButton = (tocData) => { + const [isTableOfContentsOpen, setIsTableOfContentsOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target) + ) { + setIsTableOfContentsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
    + {tocData.tocData.length !== 0 && ( +
    +
    setIsTableOfContentsOpen(!isTableOfContentsOpen)} + > + + + Table of Contents + +
    + {isTableOfContentsOpen && ( +
    + +
    + )} +
    + )} +
    + ); +}; + +export default TocOverflowButton; diff --git a/components/AppRouterMigrationComponents/Docs/docsSearch/SearchComponent.tsx b/components/AppRouterMigrationComponents/Docs/docsSearch/SearchComponent.tsx new file mode 100644 index 0000000000..a1f383dba2 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsSearch/SearchComponent.tsx @@ -0,0 +1,194 @@ +import Link from "next/link"; +import { useState, useEffect, useRef } from "react"; +import { fetchAlgoliaSearchResults } from "utils/new-search"; +import { highlightText } from "./SearchNavigation"; + +export const SearchHeader = ({ query }: { query: string }) => { + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [isSortOpen, setIsSortOpen] = useState(false); + + const filterOptions = ['FilterOp1', 'FilterOp2', 'FilterOp3']; + const sortOptions = ['Relevance', 'Newest First', 'Oldest First']; + + const toggleFilterDropdown = () => { + setIsFilterOpen(!isFilterOpen); + setIsSortOpen(false); + }; + + const toggleSortDropdown = () => { + setIsSortOpen(!isSortOpen); + setIsFilterOpen(false); + }; + + return ( +
    +
    + Results for "{query}" +
    +
    + {/* Filter Button */} +
    + {/* TODO: Implement Feature and Sort buttons - https://github.com/tinacms/tina.io/issues/2550 */} + {/* */} + {isFilterOpen && ( +
    + {filterOptions.map((option) => ( +
    { + + setIsFilterOpen(false); + }} + > + {option} +
    + ))} +
    + )} +
    + {/* TODO: Implement Feature and Sort buttons - https://github.com/tinacms/tina.io/issues/2550 */} + {/* Sort Button */} + {/*
    + + {isSortOpen && ( +
    + {sortOptions.map((option) => ( +
    { + + setIsSortOpen(false); + }} + > + {option} +
    + ))} +
    + )} +
    */} +
    +
    + ); +}; + +export const SearchTabs = ({ query }: { query: string }) => { + const [activeTab, setActiveTab] = useState('DOCS'); + const [algoliaSearchResults, setAlgoliaSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchResults = async () => { + setIsLoading(true); + setAlgoliaSearchResults(null); + if (query) { + const results = await fetchAlgoliaSearchResults(query); + setAlgoliaSearchResults(results); + } + setIsLoading(false); + }; + + fetchResults(); + }, [query]); + + const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + const activeTabIndex = activeTab === 'DOCS' ? 0 : 1; + const activeTabElement = tabRefs.current[activeTabIndex]; + const left = activeTabElement?.offsetLeft || 0; + const width = (activeTabElement?.offsetWidth || 0) + 30; + + const numberOfResults = + (algoliaSearchResults?.docs?.count + algoliaSearchResults?.blogs?.count) || 0; + + return ( +
    +
    +
    + {/* Navigation Buttons */} + + + {/* Search Results Count */} +
    + {numberOfResults}{' '} + Results +
    +
    + {isLoading && ( +
    + Mustering all the Llamas... +
    + )} + + {(numberOfResults == 0 && isLoading==false) &&
    No Results Found...
    } +
    +
    + ); +}; + + +export const SearchBody = ({ + results, + activeItem, +}: { + results: any; + activeItem: string; +}) => { + const bodyItem = activeItem === 'DOCS' ? results?.docs : results?.blogs; + + return ( +
    + {bodyItem?.results.map((item: any) => ( +
    + +

    + {highlightText(item._highlightResult.title.value)} +

    +

    + {highlightText(item._highlightResult.excerpt?.value || '')} +

    + +
    + ))} +
    + ); +}; diff --git a/components/AppRouterMigrationComponents/Docs/docsSearch/SearchNavigation.tsx b/components/AppRouterMigrationComponents/Docs/docsSearch/SearchNavigation.tsx new file mode 100644 index 0000000000..89f5bf7478 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/docsSearch/SearchNavigation.tsx @@ -0,0 +1,281 @@ +'use client'; + +import { DocsNavigationList } from '../DocumentationNavigation/DocsNavigationList'; +import { VersionSelect } from 'components/DocumentationNavigation/VersionSelect'; +import { MobileVersionSelect } from 'components/docsMain/docsMobileHeader'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useEffect, useRef, useState } from 'react'; +import { HiMagnifyingGlass } from 'react-icons/hi2'; +import { fetchAlgoliaSearchResults } from 'utils/new-search'; + +// Helper function for highlighting Algolia search hits +export const highlightText = (text: string) => { + const regex = /(.*?)<\/em>/g; + const segments = []; + let lastIndex = 0; + + let match; + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + segments.push(text.substring(lastIndex, match.index)); + } + segments.push( + + {match[1]} + + ); + lastIndex = regex.lastIndex; + } + if (lastIndex < text.length) { + segments.push(text.substring(lastIndex)); + } + return segments; +}; + +export const SearchResultsOverflowBody = ({ + results, + activeItem, + query, + numberOfResults, + isLoading, +}: { + results: any; + activeItem: string; + query: string; + numberOfResults: number; + isLoading: boolean; +}) => { + const bodyItem = activeItem === 'DOCS' ? results?.docs : results?.blogs; + + return ( +
    + {bodyItem?.results.slice(0, 10).map((item: any) => ( +
    + +

    + {highlightText(item._highlightResult.title.value)} +

    +

    + {highlightText(item._highlightResult.excerpt?.value || '')} +

    + +
    + ))} +
    + {numberOfResults > 0 ? ( + +
    + See All {numberOfResults} Results +
    + + ) : ( + !isLoading && ( +
    + No Llamas Found... +
    + ) + )} +
    +
    + ); +}; + +export const SearchResultsOverflowTabs = ({ query }) => { + const [activeTab, setActiveTab] = useState('DOCS'); + const [algoliaSearchResults, setAlgoliaSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchResults = async () => { + setIsLoading(true); + setAlgoliaSearchResults(null); + if (query) { + const results = await fetchAlgoliaSearchResults(query); + setAlgoliaSearchResults(results); + } + setIsLoading(false); + }; + + fetchResults(); + }, [query]); + + const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + const activeTabIndex = activeTab === 'DOCS' ? 0 : 1; + const activeTabElement = tabRefs.current[activeTabIndex]; + const left = activeTabElement?.offsetLeft || 0; + const width = (activeTabElement?.offsetWidth || 0) + 30; + const numberOfResults = + algoliaSearchResults?.docs?.count + algoliaSearchResults?.blogs?.count; + + return ( +
    +
    +
    + {/* Navigation Buttons */} + +
    + {isLoading && ( +
    + Mustering all the Llamas... +
    + )} +
    + +
    +
    +
    + ); +}; + +export const SearchResultsOverflow = ({ query }) => { + return ( +
    + +
    + ); +}; + +export const DocsSearchBarHeader = ({ + paddingGlobal, + headerColour, + headerPadding, + searchMargin, + searchBarPadding, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [userHasTyped, setUserHasTyped] = useState(false); + const [searchOverFlowOpen, setSearchOverflowOpen] = useState(false); + const router = useRouter(); + const headerStyling = + headerColour.toLowerCase() === 'blue' + ? 'from-blue-600/80 via-blue-800/80 to-blue-1000' + : 'from-orange-400 via-orange-500 to-orange-600'; + + const handleKeyChange = (e: React.ChangeEvent) => { + setSearchOverflowOpen(true); + const value = e.target.value; + setSearchTerm(value); + + if (value.trim()) { + setUserHasTyped(true); + fetchSearchResults(value); + } else { + setUserHasTyped(false); + setSearchResults(null); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchTerm.trim()) { + router.push(`/search?query=${encodeURIComponent(searchTerm)}`); + setSearchOverflowOpen(false); + } + }; + + const fetchSearchResults = async (query: string) => { + setIsLoading(true); + try { + const results = await fetchAlgoliaSearchResults(query); + setSearchResults(results); + } catch (error) { + console.error('Error fetching search results:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
    +
    +

    + Docs +

    +
    + +
    +
    +
    + setSearchOverflowOpen(true)} + /> + { + if (searchTerm.trim()) { + router.push(`/search?query=${encodeURIComponent(searchTerm)}`); + setSearchOverflowOpen(false); + } + }} + /> +
    + {userHasTyped && searchOverFlowOpen && ( + + )} +
    + ); +}; + +export const LeftHandSideParentContainer = ({ tableOfContents }) => { + return ( +
    + +
    + +
    +
    + ); +}; diff --git a/components/AppRouterMigrationComponents/Docs/toc/index.tsx b/components/AppRouterMigrationComponents/Docs/toc/index.tsx new file mode 100644 index 0000000000..368a17e768 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/toc/index.tsx @@ -0,0 +1,194 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import ReactMarkdown from 'react-markdown'; +import styled, { css } from 'styled-components'; +import RightArrowSvg from 'public/svg/right-arrow.svg'; +import { getDocId } from 'utils/docs/getDocIds'; + +interface TocProps { + tocItems: Array<{ type: string; text: string }>; + activeIds: string[]; +} + +export const generateMarkdown = (tocItems: Array<{ type: string; text: string }>) => { + return tocItems + .map((item) => { + const anchor = getDocId(item.text); + const prefix = item.type === 'h3' ? ' ' : ''; + return `${prefix}- [${item.text}](#${anchor})`; + }) + .join('\n'); +}; + +const ToC = ({ tocItems, activeIds }: TocProps) => { + const [isOpen, setIsOpen] = useState(false); + + useEffect(() => { + const close = () => setIsOpen(false); + const allLinks = document.querySelectorAll('a'); + allLinks.forEach((a) => a.addEventListener('click', close)); + return () => allLinks.forEach((a) => a.removeEventListener('click', close)); + }, []); + + if (!tocItems || tocItems.length === 0) { + return null; + } + + const tocMarkdown = generateMarkdown(tocItems); + + + + return ( + + + Table of Contents + ( +
  • {children}
  • + ), + a: ({ children, ...props }) => { + const isActive = activeIds.includes(props.href?.slice(1)); // Match href with activeIds + return ( + + {children} + + ); + }, + }} + > + {tocMarkdown} +
    +
    +
    + ); +}; + +export default ToC; + +const TocDesktopHeader = styled.span` + display: none; + font-size: 1rem; + color: var(--color-secondary); + opacity: 0.5; + background: transparent; + line-height: 1; + margin-bottom: 1.125rem; + + @media (min-width: 1200px) { + display: block; + } +`; + +const TocWrapper = styled.div` + margin-bottom: -0.375rem; + flex: 0 0 auto; + + @media (min-width: 1200px) { + position: sticky; + top: 8rem; + } +`; + +const TocButton = styled.button<{ isOpen: boolean }>` + display: block; + padding: 0; + outline: none; + border: none; + color: var(--color-secondary); + opacity: 0.65; + background: transparent; + cursor: pointer; + transition: opacity 185ms ease-out; + display: flex; + align-items: center; + line-height: 1; + margin-bottom: 1.125rem; + + span { + margin-right: 0.5rem; + } + + svg { + position: relative; + width: 1.25rem; + height: auto; + fill: var(--color-grey); + transform-origin: 50% 50%; + transition: opacity 180ms ease-out, transform 180ms ease-out; + opacity: 0.5; + } + + :hover, + :focus { + opacity: 1; + + svg { + opacity: 1; + } + } + + ${(props) => + props.isOpen && + css` + color: var(--color-orange); + + svg { + transform: rotate(90deg); + opacity: 1; + } + `} + + @media (min-width: 1200px) { + display: none; + } +`; + +const TocContent = styled.div<{ isOpen: boolean; activeIds: string[] }>` + display: block; + width: 100%; + line-height: 1.25; + height: auto; + max-height: 0; + overflow: hidden; + transition: all 400ms ease-out; + + ${(props) => + props.isOpen && + css` + transition: all 750ms ease-in; + max-height: 1500px; + `} + + @media (min-width: 1200px) { + max-height: none; + } + + ul { + list-style-type: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + } + + li { + margin: 0; + padding: 0.375rem 0; + } + + + ul ul { + padding-left: 0.75rem; + + li { + padding: 0.25rem 0; + } + } +`; diff --git a/components/AppRouterMigrationComponents/Docs/toc/toc-item.template.tsx b/components/AppRouterMigrationComponents/Docs/toc/toc-item.template.tsx new file mode 100644 index 0000000000..e6d21f34bf --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/toc/toc-item.template.tsx @@ -0,0 +1,15 @@ +import { Template } from "tinacms" + +export const itemTemplate: Template = { + label: 'Item', + name: 'item', + ui: { + itemProps: (item) => { + return { label: '🔗 ' + (item?.title ?? "Unnamed Menu Item") }; + }, + }, + fields: [ + { name: 'title', label: 'Name', type: 'string' }, + { name: 'slug', label: 'Page', type: 'reference', collections: ['doc'] } + ] +} \ No newline at end of file diff --git a/components/AppRouterMigrationComponents/Docs/toc/toc-submenu.template.tsx b/components/AppRouterMigrationComponents/Docs/toc/toc-submenu.template.tsx new file mode 100644 index 0000000000..c5f4b2f0d2 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/toc/toc-submenu.template.tsx @@ -0,0 +1,36 @@ +import { Template } from "tinacms" +import { itemTemplate } from "./toc-item.template" + +const uiAndLabelling: any = { + label: 'Submenu', + name: 'items', + ui: { + itemProps: (item) => { + return { label: '🗂️ ' + (item?.title ?? "Unnamed Menu Group") }; + }, + }, +} + +const thirdLevelSubmenu: Template = { + ...uiAndLabelling, + fields: [ + { name: 'title', label: 'Name', type: 'string' }, + { name: 'items', label: 'Submenu Items', type: 'object', list: true, templates: [itemTemplate] } + ] +} + +const secondLevelSubmenu: Template = { + ...uiAndLabelling, + fields: [ + { name: 'title', label: 'Name', type: 'string' }, + { name: 'items', label: 'Submenu Items', type: 'object', list: true, templates: [thirdLevelSubmenu, itemTemplate] } + ] +} + +export const submenuTemplate: Template = { + ...uiAndLabelling, + fields: [ + { name: 'title', label: 'Name', type: 'string' }, + { name: 'items', label: 'Submenu Items', type: 'object', list: true, templates: [secondLevelSubmenu, itemTemplate] } + ] +} \ No newline at end of file diff --git a/components/AppRouterMigrationComponents/Docs/toc_helper.ts b/components/AppRouterMigrationComponents/Docs/toc_helper.ts new file mode 100644 index 0000000000..6d791a5cd7 --- /dev/null +++ b/components/AppRouterMigrationComponents/Docs/toc_helper.ts @@ -0,0 +1,66 @@ +import React from 'react'; + +interface Heading { + id?: string; + offset?: number; + level?: string; +} + +function createHeadings( + contentRef: React.RefObject +): Heading[] { + const headings: Heading[] = []; + const htmlElements = contentRef.current?.querySelectorAll( + 'h1, h2, h3, h4, h5, h6' + ); + + htmlElements?.forEach((heading: HTMLHeadingElement) => { + headings.push({ + id: heading.id, + offset: heading.offsetTop, + level: heading.tagName, + }); + }); + return headings; +} + +export function createTocListener( + contentRef: React.RefObject, + setActiveIds: (activeIds: string[]) => void +): () => void { + const headings = createHeadings(contentRef); + + return function onScroll(): void { + const scrollPos = window.scrollY + window.innerHeight / 4; // Adjust for active detection + const activeIds: string[] = []; + + headings.forEach((heading) => { + if (heading.offset && scrollPos >= heading.offset) { + activeIds.push(heading.id ?? ''); + } + }); + + setActiveIds(activeIds); + }; +} + +export function useTocListener(data: any) { + const [activeIds, setActiveIds] = React.useState([]); + const contentRef = React.useRef(null); + + React.useEffect(() => { + if (!contentRef.current) return; + + const tocListener = createTocListener(contentRef, setActiveIds); + const handleScroll = () => tocListener(); // Define scroll handler + + window.addEventListener('scroll', handleScroll); + handleScroll(); // Initialize active IDs on mount + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [data]); + + return { contentRef, activeIds }; +} diff --git a/components/DocumentationNavigation/DocsNavigationList.tsx b/components/DocumentationNavigation/DocsNavigationList.tsx index 520c3de4d9..5fbd2d72b1 100644 --- a/components/DocumentationNavigation/DocsNavigationList.tsx +++ b/components/DocumentationNavigation/DocsNavigationList.tsx @@ -242,7 +242,6 @@ const NavLabelContainer = styled.div<{ status: string }>` `; export const DocsNavigationList = ({ navItems }: DocsNavProps) => { - const router = useRouter(); const navListElem = React.useRef(null); return ( diff --git a/components/blocks/CardGrid.tsx b/components/blocks/CardGrid.tsx index 7c20a3ecbe..f0f5a09a7d 100644 --- a/components/blocks/CardGrid.tsx +++ b/components/blocks/CardGrid.tsx @@ -1,90 +1,35 @@ -import Link from 'next/link'; import { Actions } from '../blocks/ActionButton/ActionsButton'; export const CardGrid = ({ props }) => { return ( - <> - - - + ); }; diff --git a/components/docsMain/docsMainBody.tsx b/components/docsMain/docsMainBody.tsx index e8f06208c9..6ca46a7d89 100644 --- a/components/docsMain/docsMainBody.tsx +++ b/components/docsMain/docsMainBody.tsx @@ -3,7 +3,6 @@ import DocsMobileHeader from './docsMobileHeader'; const MainDocsBodyHeader = (docData) => { const DocumentTitle = docData.data.new.results.data.doc.title; - return (
    {docData.screenSizing && ( diff --git a/components/hooks/ScreenResizer.tsx b/components/hooks/ScreenResizer.tsx new file mode 100644 index 0000000000..e6d963f337 --- /dev/null +++ b/components/hooks/ScreenResizer.tsx @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; + +export function screenResizer() { + const [isScreenSmallerThan1200, setIsScreenSmallerThan1200] = useState(false); + const [isScreenSmallerThan840, setIsScreenSmallerThan840] = useState(false); + + useEffect(() => { + const updateScreenSize = () => { + setIsScreenSmallerThan1200(window.innerWidth < 1200); + setIsScreenSmallerThan840(window.innerWidth < 840); + }; + + updateScreenSize(); + + window.addEventListener('resize', updateScreenSize); + + return () => window.removeEventListener('resize', updateScreenSize); + }, []); + + return { isScreenSmallerThan1200, isScreenSmallerThan840 }; +} \ No newline at end of file diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx index 8a86d6b43e..d4ae8d3f24 100644 --- a/components/layout/Footer.tsx +++ b/components/layout/Footer.tsx @@ -88,9 +88,6 @@ const SocialLink = ({ link, children }) => { }; export const Footer = () => { - useEffect(() => { - console.log(FooterData); - }, []); return (
    diff --git a/components/tinaMarkdownComponents/docAndBlogComponents.tsx b/components/tinaMarkdownComponents/docAndBlogComponents.tsx index 9ebc4bda59..0c7233312b 100644 --- a/components/tinaMarkdownComponents/docAndBlogComponents.tsx +++ b/components/tinaMarkdownComponents/docAndBlogComponents.tsx @@ -384,7 +384,7 @@ function FormatHeaders({ children, level }) { const styles = { 1: 'bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent text-4xl mt-4 mb-4', 2: 'bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent text-3xl mt-4 mb-3', - 3: 'bg-gradient-to-br from-blue-800 via-blue-900 to-blue-100 bg-clip-text text-transparent text-xl font-medium mt-2 mb-2', + 3: 'bg-gradient-to-br from-blue-800 via-blue-900 to-blue-100 bg-clip-text text-transparent text-xl font-medium mt-2 mb-2 !important', 4: 'bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent text-xl font-medium mt-2 mb-2', 5: 'bg-gradient-to-br from-orange-400 via-orange-500 to-orange-600 bg-clip-text text-transparent text-lg font-medium mt-2 mb-1', 6: 'text-gray-500 text-base font-normal mt-2 mb-1', diff --git a/components/tinaMarkdownComponents/templateComponents/scrollBasedShowcase.tsx b/components/tinaMarkdownComponents/templateComponents/scrollBasedShowcase.tsx index 2d6da4be9f..77d5813cb2 100644 --- a/components/tinaMarkdownComponents/templateComponents/scrollBasedShowcase.tsx +++ b/components/tinaMarkdownComponents/templateComponents/scrollBasedShowcase.tsx @@ -1,35 +1,38 @@ -import React, { useEffect, useRef } from 'react'; -import styled from 'styled-components'; +import React, { useEffect, useRef, useState } from 'react'; import { TinaMarkdown } from 'tinacms/dist/rich-text'; -import { docAndBlogComponents } from '../docAndBlogComponents'; +/** Minimal inline docAndBlogComponents for headings only */ +const docAndBlogComponents = { + h2: (props: any) =>

    , + h3: (props: any) =>

    , +}; + +/** UseWindowSize Hook */ function useWindowSize() { if (typeof window !== 'undefined') { return { width: 1200, height: 800 }; } + const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(); - const [windowSize, setWindowSize] = React.useState<{ - width: number; - height: number; - }>(); - - React.useEffect(() => { - window.addEventListener('resize', () => { + useEffect(() => { + const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); - }); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); }, []); return windowSize; } +/** Throttled scroll listener */ function createListener( componentRef: React.RefObject, headings: Item[], setActiveIds: (activeIds: string[]) => void -): () => void { +) { let tick = false; const THROTTLE_INTERVAL = 100; - //Find maximum relative scroll position const maxScrollY = document.documentElement.scrollHeight - window.innerHeight; const maxScrollYRelative = @@ -44,7 +47,6 @@ function createListener( return { ...heading, - //Find the relative position of the heading based on the page content. relativePagePosition: maxScrollYRelative > 1 ? relativePosition @@ -53,23 +55,14 @@ function createListener( }); const throttledScroll = () => { - if (!componentRef.current) { - return; - } - //Find the current vertical scroll pixel value + if (!componentRef.current) return; const scrollPos = window.scrollY - componentRef.current.offsetTop + window.innerHeight / 6; - const newActiveIds = []; - //Find the relative position on the page based on the scroll. - const relativeScrollPosition = - scrollPos / componentRef.current.scrollHeight; + const newActiveIds: string[] = []; + const relativeScrollPosition = scrollPos / componentRef.current.scrollHeight; - //Find the headings that are above the current scroll position - //This is adjusted to account for differences between min/max scroll values and content height const activeHeadingCandidates = relativePositionHeadingMap.filter( - (heading) => { - return relativeScrollPosition >= heading.relativePagePosition; - } + (heading) => relativeScrollPosition >= heading.relativePagePosition ); const activeHeading = @@ -78,14 +71,13 @@ function createListener( prev.offset > current.offset ? prev : current ) : headings[0] ?? {}; + newActiveIds.push(activeHeading.id); - if (activeHeading.level != 'H2') { + if (activeHeading.level !== 'H2') { const activeHeadingParentCandidates = activeHeadingCandidates.length > 0 - ? activeHeadingCandidates.filter((heading) => { - return heading.level == 'H2'; - }) + ? activeHeadingCandidates.filter((h) => h.level === 'H2') : []; const activeHeadingParent = activeHeadingParentCandidates.length > 0 @@ -101,9 +93,9 @@ function createListener( setActiveIds(newActiveIds); }; - return function onScroll(): void { + return function onScroll() { if (!tick) { - setTimeout(function () { + setTimeout(() => { throttledScroll(); tick = false; }, THROTTLE_INTERVAL); @@ -119,19 +111,26 @@ interface Item { src?: string; } -export default function ScrollBasedShowcase(data) { - const [headings, setHeadings] = React.useState([]); - const componentRef = useRef(null); - const activeImg = useRef(null); - +/** Main Component */ +export default function ScrollBasedShowcase(data: { + showcaseItems: { + title: string; + image: string; + content: any; + useAsSubsection?: boolean; + }[]; +}) { + const [headings, setHeadings] = useState([]); + const componentRef = useRef(null); + const activeImg = useRef(null); const headingRefs = useRef<(HTMLHeadingElement | null)[]>([]); - - const [activeIds, setActiveIds] = React.useState([]); + const [activeIds, setActiveIds] = useState([]); const windowSize = useWindowSize(); + /** Build headings array on mount */ useEffect(() => { - const tempHeadings = []; + const tempHeadings: Item[] = []; data.showcaseItems?.forEach((item, index) => { const headingData: Item = { id: `${item.title}-${index}`, @@ -144,243 +143,122 @@ export default function ScrollBasedShowcase(data) { setHeadings(tempHeadings); }, [data.showcaseItems]); + /** Update heading offsets on resize */ useEffect(() => { const updateOffsets = () => { - const updatedHeadings = headings.map((heading, index) => { - return { - ...heading, - offset: headingRefs.current[index]?.offsetTop ?? 0, - }; - }); - + const updatedHeadings = headings.map((heading, index) => ({ + ...heading, + offset: headingRefs.current[index]?.offsetTop ?? 0, + })); setHeadings(updatedHeadings); }; - window.addEventListener('resize', updateOffsets); return () => window.removeEventListener('resize', updateOffsets); }, [headings]); - React.useEffect(() => { - if (typeof window === `undefined`) { - return; - } - - const activeTocListener = createListener( - componentRef, - headings, - setActiveIds - ); + /** Throttled scroll event */ + useEffect(() => { + if (typeof window === 'undefined') return; + const activeTocListener = createListener(componentRef, headings, setActiveIds); window.addEventListener('scroll', activeTocListener); - return () => window.removeEventListener('scroll', activeTocListener); - }, [headings, windowSize, componentRef]); + }, [headings, windowSize]); + /** Update active image when activeIds change */ useEffect(() => { - if (typeof window === 'undefined') return; - if (!activeIds.length) { - return; + if (!activeIds.length) return; + const heading = headings.find((h) => h.id === activeIds[0]); + if (activeImg.current) { + activeImg.current.src = heading?.src || ''; } - - const heading = headings.find((heading) => heading.id === activeIds[0]); - - activeImg.current.src = heading?.src; - }, [activeIds, activeImg]); + }, [activeIds, headings]); console.log( componentRef.current?.scrollHeight - - headings.filter((heading) => activeIds.includes(heading.id))[ - activeIds.length - 1 - ]?.offset - + headings.filter((h) => activeIds.includes(h.id))[activeIds.length - 1]?.offset - activeImg.current?.scrollHeight ); return ( - - -
    +
    +
    +
    {data.showcaseItems?.map((item, index) => { + const itemId = `${item.title}-${index}`; + const isFocused = activeIds.includes(itemId); + return (
    full opacity + orange border + text color + // If not => half opacity + gray border + className={`mt-0 md:mt-8 transition-all duration-300 ease-in-out + ${isFocused + ? 'opacity-100 text-gray-900' + : 'opacity-15 border-gray-300 text-gray-800' + } + `} > {item.useAsSubsection ? (
    (headingRefs.current[index] = el)} > - - {item.title} - +
    {item.title}
    ) : (
    (headingRefs.current[index] = el)} > - - {item.title} - +

    {item.title}

    )} -
      + +
      • - +
      -
      - -
      + + {/* This image is only shown on mobile (md:hidden). + On larger screens, the separate container is used. */} + {item.title}
    ); })}
    -
    - {/* Im keeping this as a .gif rather than transferring to .webm as a .gif would require me to change from to
    +
    ); } - -export const DocContainer = styled.div` - display: block; - width: 100%; - position: relative; - padding: 1rem 2rem 3rem 2rem; - margin: 5rem auto; -`; - -const MAX_SPLIT_IMG_WIDTH = 768; -const SplitContent = styled.div` - display: flex; - position: relative; - min-height: 100vh; - - h3 { - font-size: 1.2rem; - } - - > * { - flex: 1; - margin: 0 10px; - padding: 10px; - box-sizing: border-box; - } - - @media (min-width: ${MAX_SPLIT_IMG_WIDTH + 1}px) { - #main-content-container > h3:not(:first-child), - #main-content-container > h2:not(:first-child) { - margin-top: 4.5rem !important; - } - - .showcase-head-wrapper { - margin-top: 2rem; - } - - ul { - list-style: none; - padding-left: 1rem; - border-left: 4px solid var(--color-light-dark); - - color: var(--color-light-dark); - transition: color 0.5s ease-in-out; - * { - color: var(--color-light-dark); - - } - } - - p { - transition: color 0.1s ease-in-out; - } - - a { - transition: all 0.5s ease-in-out; - pointer-events: none; - } - - h2, h3 { - background-clip: unset; - background-image: none; - } - - .showcase-heading { - color: var(--color-light-dark); - - &:not(.focused) * { - color: var(--color-light-dark); - } - } - - div.focused a, - div.focused a { - color: var(--color-orange) !important; - } - - div.focused > p, - div.focused > ul { - border-left: 4px solid var(--color-orange); - - color: var(--color-primary); - * { - color: var(--color-primary); - } - } - } - } - - #main-content-container img { - display: none; - } - - @media (max-width: ${MAX_SPLIT_IMG_WIDTH}px) { - .img-container { - display: none; - } - - #main-content-container img { - display: initial; - margin: 2rem 0 - } - } - - .transition-img { - position: absolute; - right: 0; - width: 50%; - transition: all 1s ease-in-out; - border-radius: 10px; - } - } - - .img-container { - position: relative; - width: 100%; - flex: 1; - } -`; diff --git a/components/toc/index.tsx b/components/toc/index.tsx index f9e06c36d5..21822afe13 100644 --- a/components/toc/index.tsx +++ b/components/toc/index.tsx @@ -41,6 +41,7 @@ const ToC = ({ tocItems, activeIds }: TocProps) => { const tocMarkdown = generateMarkdown(tocItems); + return ( setIsOpen(!isOpen)}> diff --git a/pages/docs/[...slug].tsx b/pages/docs/[...slug].tsx deleted file mode 100644 index 60c6c6d1f2..0000000000 --- a/pages/docs/[...slug].tsx +++ /dev/null @@ -1,321 +0,0 @@ -import { Breadcrumbs } from 'components/DocumentationNavigation/Breadcrumbs'; -import MainDocsBodyHeader from 'components/docsMain/docsMainBody'; -import TocOverflowButton from 'components/docsMain/tocOverflowButton'; -import { LeftHandSideParentContainer } from 'components/docsSearch/SearchNavigation'; -import { DocsLayout, Layout, MarkdownContent } from 'components/layout'; -import { docAndBlogComponents } from 'components/tinaMarkdownComponents/docAndBlogComponents'; -import ToC from 'components/toc/index'; -import { DocsPagination, LastEdited, NavToggle } from 'components/ui'; -import { GetStaticPaths, GetStaticProps } from 'next'; -import { NextSeo } from 'next-seo'; -import Error from 'next/error'; -import { useRouter } from 'next/router'; -import { format } from 'path'; -import { doc } from 'prettier'; -import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; -import client from 'tina/__generated__/client'; -import { useTina } from 'tinacms/dist/react'; -import { TinaMarkdown } from 'tinacms/dist/rich-text'; -import { getDocsNav } from 'utils/docs/getDocProps'; -import { getSeoDescription } from 'utils/docs/getSeoDescription'; -import getTableOfContents from 'utils/docs/getTableOfContents'; -import { NotFoundError } from 'utils/error/NotFoundError'; -import { openGraphImage } from 'utils/open-graph-image'; -import { useTocListener } from 'utils/toc_helpers'; -import * as ga from '../../utils/ga'; - -export function DocTemplate(props) { - return <_DocTemplate {...props} />; -} - -function screenResizer() { - const [isScreenSmallerThan1200, setIsScreenSmallerThan1200] = useState(false); - const [isScreenSmallerThan840, setIsScreenSmallerThan840] = useState(false); - - useEffect(() => { - const updateScreenSize = () => { - setIsScreenSmallerThan1200(window.innerWidth < 1200); - setIsScreenSmallerThan840(window.innerWidth < 840); - }; - - updateScreenSize(); - - window.addEventListener('resize', updateScreenSize); - - return () => window.removeEventListener('resize', updateScreenSize); - }, []); - - return { isScreenSmallerThan1200, isScreenSmallerThan840 }; -} - -function _DocTemplate(props) { - // fallback workaround - if (props.notFound) { - return ; - } - - const { data } = useTina({ - query: props.new?.results.query, - data: props.new?.results.data, - variables: props.new?.results.variables, - }); - - const router = useRouter(); - - const doc_data = data.doc; - const previousPage = { - slug: doc_data.previous?.id.slice(7, -4), - title: doc_data.previous?.title, - }; - const nextPage = { - slug: doc_data.next?.id.slice(7, -4), - title: doc_data.next?.title, - }; - const TableOfContents = getTableOfContents(doc_data.body.children); - const description = - doc_data.seo?.description?.trim() || getSeoDescription(doc_data.body); - const title = doc_data.seo?.title || doc_data.title; - const { activeIds, contentRef } = useTocListener(doc_data); - const lastEdited = props.new.results.data.doc.last_edited; - const date = lastEdited === null ? null : new Date(lastEdited); - const formattedDate = date - ? date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) - : ''; - - const isScreenSmallerThan1200 = screenResizer().isScreenSmallerThan1200; - const isScreenSmallerThan840 = screenResizer().isScreenSmallerThan840; - const gridClass = isScreenSmallerThan840 - ? 'grid-cols-1' - : isScreenSmallerThan1200 - ? 'grid-cols-[1.25fr_3fr]' - : 'grid-cols-[1.25fr_3fr_0.75fr]'; - - useEffect(() => { - const handleRouteChange = (url) => { - ga.pageview(url); - }; - //When the component is mounted, subscribe to router changes - //and log those page views - router.events.on('routeChangeComplete', handleRouteChange); - - // If the component is unmounted, unsubscribe - // from the event with the `off` method - return () => { - router.events.off('routeChangeComplete', handleRouteChange); - }; - }, [router.events]); - - return ( - <> - - -
    -
    - {/* LEFT COLUMN */} -
    - -
    - {/* MIDDLE COLUMN */} -
    - - {isScreenSmallerThan1200 && !doc_data.tocIsHidden && ( - - )} -
    - - - {formattedDate && ( - - {' '} - Last Edited: {formattedDate} - - )} - - -
    -
    - {/* RIGHT COLUMN */} - {doc_data.tocIsHidden ? null : ( -
    - -
    - )} -
    -
    -
    - - ); -} - -export default DocTemplate; - -/* - * DATA FETCHING ------------------------------------------------------ - */ - -export const getStaticProps: GetStaticProps = async function (props) { - let { slug: slugs } = props.params; - - // @ts-ignore This should maybe always be a string[]? - const slug = slugs.join('/'); - - try { - const [results, navDocData] = await Promise.all([ - client.queries.doc({ relativePath: `${slug}.mdx` }), - getDocsNav(), - ]); - return { - props: { - new: { results }, - navDocData, - }, - }; - } catch (e) { - if (e) { - return { - props: { - error: { ...e }, //workaround since we cant return error as JSON - }, - }; - } else if (e instanceof NotFoundError) { - return { - props: { - notFound: true, - }, - }; - } - } -}; - -export const getStaticPaths: GetStaticPaths = async function () { - const fg = require('fast-glob'); - const contentDir = './content/docs/'; - const files = await fg(`${contentDir}**/*.mdx`); - return { - fallback: false, - paths: files - .filter((file) => !file.endsWith('index.mdx')) - .map((file) => { - const path = file.substring(contentDir.length, file.length - 4); - return { params: { slug: path.split('/') } }; - }), - }; -}; - -/* - * STYLES -------------------------------------------------------------- - */ - -export const DocsGrid = styled.div` - display: block; - width: 100%; - position: relative; - padding: 1rem 2rem 3rem 2rem; - max-width: 768px; - margin: 0 auto; - - @media (min-width: 500px) { - padding: 1rem 3rem 4rem 3rem; - } - - @media (min-width: 1200px) { - display: grid; - max-width: none; - padding: 2rem 0rem 4rem 0rem; - grid-template-areas: - '. header header .' - '. content toc .'; - grid-auto-columns: minmax(0, auto) minmax(300px, 800px) - clamp(17.5rem, 10rem + 10vw, 21.25rem) minmax(0, auto); - grid-column-gap: 5rem; - justify-content: left; - } -`; - -export const DocGridHeader = styled.div` - grid-area: header; - width: 100%; -`; - -export const DocGridToc = styled.div` - grid-area: toc; - width: 100%; - - @media (min-width: 1200px) { - padding-top: 4.5rem; - } -`; - -interface ContentProps { - ref: any; -} - -export const DocGridContent = styled.div` - grid-area: content; - width: 100%; -`; - -export const DocsPageTitle = styled.h1` - font-size: 2rem; - line-height: 1.2 !important; - letter-spacing: 0.1px; - color: var(--color-orange); - position: relative; - font-family: var(--font-tuner); - font-style: normal; - - margin: 0 0 0 0 !important; - - @media (max-width: 1199px) { - margin: 0 0 1.25rem 0 !important; - } -`; - -export const DocsNavToggle = styled(NavToggle)` - position: fixed; - margin-top: 1.25rem; - left: 1rem; - z-index: 500; - - @media (min-width: 999px) { - display: none; - } -`; diff --git a/pages/docs/index.tsx b/pages/docs/index.tsx deleted file mode 100644 index 000d83e67b..0000000000 --- a/pages/docs/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { GetStaticProps } from 'next'; -import React from 'react'; -import DocTemplate, { - getStaticPaths as slugGetStaticPaths, - getStaticProps as slugGetStaticProps, -} from './[...slug]'; - -export const getStaticProps: GetStaticProps = async function (props) { - return await slugGetStaticProps({ params: { slug: ['index'] } }); -}; - -export default DocTemplate;