mirror of https://github.com/lukechilds/docs.git
12 changed files with 432 additions and 110 deletions
@ -1,26 +1,66 @@ |
|||||
import { convertRemoteDataToMDX } from '@common/data/mdx'; |
import { convertRemoteDataToMDX } from '@common/data/mdx'; |
||||
|
|
||||
import FAQ_JSON from '../../_data/faqs.json'; |
|
||||
import TurndownService from 'turndown'; |
import TurndownService from 'turndown'; |
||||
|
import { slugify } from '@common/utils'; |
||||
|
import { getBetterNames } from '@common/utils/faqs'; |
||||
|
|
||||
|
const fetchSections = async () => { |
||||
|
const res = await fetch('https://blockstack.zendesk.com/api/v2/help_center/en-us/sections.json'); |
||||
|
const { sections } = await res.json(); |
||||
|
return sections; |
||||
|
}; |
||||
|
const fetchArticles = async (id: number) => { |
||||
|
const res = await fetch( |
||||
|
`https://blockstack.zendesk.com/api/v2/help_center/en-us/sections/${id}/articles.json?per_page=100` |
||||
|
); |
||||
|
const { articles } = await res.json(); |
||||
|
return articles; |
||||
|
}; |
||||
|
|
||||
|
// This function gets called at build time
|
||||
|
export async function getStaticPaths() { |
||||
|
const sections = await fetchSections(); |
||||
|
const paths = sections.map(section => ({ |
||||
|
params: { slug: slugify(getBetterNames(section.id).title) }, |
||||
|
})); |
||||
|
|
||||
|
return { paths, fallback: false }; |
||||
|
} |
||||
|
|
||||
|
const getSectionBySlug = (sections, slug) => |
||||
|
sections.find(s => { |
||||
|
const { title } = getBetterNames(s.id); |
||||
|
const _slug = slugify(title); |
||||
|
return _slug === slug; |
||||
|
}); |
||||
|
|
||||
export const convertFaqAnswersToMDX = async () => { |
export async function getStaticProps(context) { |
||||
|
const sections = await fetchSections(); |
||||
|
let articles = []; |
||||
|
|
||||
|
if (context?.params?.slug) { |
||||
|
const section = getSectionBySlug(sections, context.params.slug); |
||||
|
const _articles = await fetchArticles(section.id); |
||||
const turndownService = new TurndownService(); |
const turndownService = new TurndownService(); |
||||
// we convert html to markdown so we can process it with remark,
|
// we convert html to markdown so we can process it with remark/rehype,
|
||||
// eg external links open in new window
|
// eg external links open in new window
|
||||
const md = FAQ_JSON.faqs.map(faq => ({ |
const md = _articles.map(faq => ({ |
||||
...faq, |
...faq, |
||||
answer: turndownService.turndown(faq.answer), |
body: turndownService.turndown(faq.body), |
||||
})); |
})); |
||||
// convert it to MDX with next-mdx-remote
|
// convert it to MDX with next-mdx-remote
|
||||
const answers = await convertRemoteDataToMDX(md, 'answer'); |
const body = await convertRemoteDataToMDX(md, 'body'); |
||||
const faqs = FAQ_JSON.faqs.map((faq, index) => ({ |
articles = _articles.map((faq, index) => ({ |
||||
...faq, |
...faq, |
||||
answer: answers[index], |
body: body[index], |
||||
})); |
})); |
||||
|
} |
||||
|
|
||||
return { |
return { |
||||
props: { |
props: { |
||||
mdx: faqs, |
sections, |
||||
|
articles, |
||||
|
...context, |
||||
}, |
}, |
||||
|
revalidate: 60 * 60 * 12, // 12 hours
|
||||
}; |
}; |
||||
}; |
} |
||||
|
@ -0,0 +1,34 @@ |
|||||
|
export const getBetterNames = (id: number) => { |
||||
|
switch (id) { |
||||
|
case 360007620914: |
||||
|
return { |
||||
|
title: 'General information', |
||||
|
description: 'General questions about Blockstack and the Stacks network', |
||||
|
img: '/images/pages/testnet.svg', |
||||
|
}; |
||||
|
case 360007411853: |
||||
|
return { |
||||
|
title: 'Stacks Token', |
||||
|
description: 'Questions relating to the native token of Stacks 2.0', |
||||
|
img: '/images/pages/mining.svg', |
||||
|
}; |
||||
|
case 360007760554: |
||||
|
return { |
||||
|
title: 'Stacks blockchain', |
||||
|
description: 'Learn about the blockchain and details of Stacks 2.0', |
||||
|
img: '/images/pages/hello-world.svg', |
||||
|
}; |
||||
|
case 360007781533: |
||||
|
return { |
||||
|
title: 'Ecosystem details', |
||||
|
description: 'Questions related to the age of the project and the contributors', |
||||
|
img: '/images/pages/data-storage.svg', |
||||
|
}; |
||||
|
case 360007780033: |
||||
|
return { |
||||
|
title: 'Building apps', |
||||
|
description: 'Learn about the platform and questions related to decentralized applications', |
||||
|
img: '/images/pages/connect.svg', |
||||
|
}; |
||||
|
} |
||||
|
}; |
@ -0,0 +1,35 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Flex, Box, color, space } from '@blockstack/ui'; |
||||
|
import ArrowLeftIcon from 'mdi-react/ArrowLeftIcon'; |
||||
|
import { Text } from '@components/typography'; |
||||
|
import Link from 'next/link'; |
||||
|
|
||||
|
const Wrapper = ({ href, children }) => |
||||
|
href ? ( |
||||
|
<Link href={href} passHref> |
||||
|
{children} |
||||
|
</Link> |
||||
|
) : ( |
||||
|
children |
||||
|
); |
||||
|
|
||||
|
export const BackButton = ({ href, ...rest }) => ( |
||||
|
<Wrapper href={href}> |
||||
|
<Flex |
||||
|
color={color('text-caption')} |
||||
|
_hover={{ |
||||
|
cursor: 'pointer', |
||||
|
color: color('text-title'), |
||||
|
}} |
||||
|
align="center" |
||||
|
as={href ? 'a' : 'div'} |
||||
|
display="flex !important" |
||||
|
{...rest} |
||||
|
> |
||||
|
<Box as="span" mr={space('extra-tight')}> |
||||
|
<ArrowLeftIcon size="16px" /> |
||||
|
</Box> |
||||
|
<Text color={'currentColor'}>Back</Text> |
||||
|
</Flex> |
||||
|
</Wrapper> |
||||
|
); |
@ -1,76 +1,68 @@ |
|||||
import React from 'react'; |
import React from 'react'; |
||||
import { Components } from '@components/mdx'; |
import { Box, space, color, Grid } from '@blockstack/ui'; |
||||
import { Box, Flex, ChevronIcon, space, color } from '@blockstack/ui'; |
import { Text } from '@components/typography'; |
||||
import hydrate from 'next-mdx-remote/hydrate'; |
|
||||
import { Accordion, AccordionItem, AccordionButton, AccordionPanel } from '@reach/accordion'; |
|
||||
import { border } from '@common/utils'; |
|
||||
import { slugify } from '@common/utils'; |
import { slugify } from '@common/utils'; |
||||
import { css } from '@styled-system/css'; |
import { css } from '@styled-system/css'; |
||||
import { useRouter } from 'next/router'; |
import { getCapsizeStyles, getHeadingStyles } from '@components/mdx/typography'; |
||||
import { useActiveHeading } from '@common/hooks/use-active-heading'; |
import { HoverImage } from '../hover-image'; |
||||
const getSlug = (asPath: string) => { |
import { useTouchable } from '@common/hooks/use-touchable'; |
||||
if (asPath.includes('#')) { |
import Link from 'next/link'; |
||||
const slug = asPath.split('#')[1]; |
import { getBetterNames } from '@common/utils/faqs'; |
||||
return slug; |
|
||||
} |
|
||||
return; |
|
||||
}; |
|
||||
|
|
||||
const FAQItem = React.memo(({ faq, ...rest }: any) => { |
|
||||
const id = slugify(faq.question); |
|
||||
const { isActive } = useActiveHeading(id); |
|
||||
|
|
||||
|
const FloatingLink = ({ href, ...props }: any) => ( |
||||
|
<Link href={href} {...props} passHref> |
||||
|
<Box as="a" position="absolute" size="100%" zIndex={999} left={0} top={0} /> |
||||
|
</Link> |
||||
|
); |
||||
|
const SectionCard = ({ section }) => { |
||||
|
const { hover, active, bind } = useTouchable({ |
||||
|
behavior: 'button', |
||||
|
}); |
||||
|
const { title, description, img } = getBetterNames(section.id); |
||||
return ( |
return ( |
||||
<Box as={AccordionItem} borderBottom={border()} {...rest}> |
<Box |
||||
<Flex |
color={color('text-title')} |
||||
as={AccordionButton} |
_hover={{ cursor: 'pointer', color: color('accent') }} |
||||
_hover={{ color: color('accent') }} |
position="relative" |
||||
|
{...bind} |
||||
|
> |
||||
|
<FloatingLink href="/references/faqs/[slug]" as={`/references/faqs/${slugify(title)}`} /> |
||||
|
<HoverImage isHovered={hover || active} src={img} /> |
||||
|
<Box> |
||||
|
<Text |
||||
|
css={css({ |
||||
|
color: 'currentColor', |
||||
|
...getHeadingStyles('h3'), |
||||
|
})} |
||||
|
> |
||||
|
{title} |
||||
|
</Text> |
||||
|
<Box> |
||||
|
<Text |
||||
css={css({ |
css={css({ |
||||
display: 'flex', |
display: 'block', |
||||
width: '100%', |
color: color('text-body'), |
||||
outline: 'none', |
mt: space('base-loose'), |
||||
alignItems: 'center', |
...getCapsizeStyles(16, 26), |
||||
justifyContent: 'space-between', |
|
||||
py: space('extra-loose'), |
|
||||
textAlign: 'left', |
|
||||
pr: space('extra-loose'), |
|
||||
color: isActive ? color('accent') : color('text-title'), |
|
||||
'& > h4': { |
|
||||
pl: space('extra-loose'), |
|
||||
}, |
|
||||
':hover': { |
|
||||
color: color('accent'), |
|
||||
}, |
|
||||
})} |
})} |
||||
> |
> |
||||
<Components.h4 id={id} my="0px !important" color="currentColor"> |
{description} |
||||
{faq.question} |
</Text> |
||||
</Components.h4> |
|
||||
<Box color={color('text-caption')} pl={space('base-loose')}> |
|
||||
<ChevronIcon direction="down" size="22px" /> |
|
||||
</Box> |
</Box> |
||||
</Flex> |
|
||||
<Box px={space('extra-loose')} pb={space('extra-loose')} as={AccordionPanel}> |
|
||||
{hydrate(faq.answer, Components)} |
|
||||
</Box> |
</Box> |
||||
</Box> |
</Box> |
||||
); |
); |
||||
}); |
}; |
||||
export const FAQs = React.memo(({ category, data }: any) => { |
export const FAQs = React.memo(({ articles, sections }: any) => { |
||||
const router = useRouter(); |
|
||||
const slug = getSlug(router.asPath); |
|
||||
const faqs = data.filter(faq => faq.category === category); |
|
||||
const slugIndex = faqs.findIndex(faq => slugify(faq.question) === slug); |
|
||||
const [index, setIndex] = React.useState(slugIndex !== -1 ? slugIndex : undefined); |
|
||||
const handleIndexChange = (value: number) => { |
|
||||
setIndex(value); |
|
||||
}; |
|
||||
|
|
||||
return ( |
return ( |
||||
<Accordion multiple collapsible defaultIndex={index} onChange={handleIndexChange}> |
<Grid |
||||
{faqs.map((faq, _index) => { |
gridTemplateColumns="repeat(2, 1fr)" |
||||
return <FAQItem faq={faq} key={_index} />; |
gridColumnGap={space('extra-loose')} |
||||
|
gridRowGap="64px" |
||||
|
> |
||||
|
{sections.map(section => { |
||||
|
return <SectionCard key={section.id} section={section} />; |
||||
})} |
})} |
||||
</Accordion> |
</Grid> |
||||
); |
); |
||||
}); |
}); |
||||
|
@ -0,0 +1,58 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Box, BoxProps, Grid, space } from '@blockstack/ui'; |
||||
|
import { transition } from '@common/utils'; |
||||
|
import { Img } from '@components/mdx/image'; |
||||
|
|
||||
|
const Image = ({ |
||||
|
src, |
||||
|
isHovered, |
||||
|
size, |
||||
|
...rest |
||||
|
}: BoxProps & { src?: string; isHovered?: boolean }) => ( |
||||
|
<Box |
||||
|
flexShrink={0} |
||||
|
style={{ |
||||
|
willChange: 'transform', |
||||
|
}} |
||||
|
width="100%" |
||||
|
size={size} |
||||
|
{...rest} |
||||
|
> |
||||
|
<Img |
||||
|
flexShrink={0} |
||||
|
borderRadius="12px" |
||||
|
src={src} |
||||
|
width="100%" |
||||
|
minWidth={size} |
||||
|
size={size} |
||||
|
mx="0 !important" |
||||
|
my="0 !important" |
||||
|
/> |
||||
|
</Box> |
||||
|
); |
||||
|
export const HoverImage: React.FC<BoxProps & { isHovered?: boolean; src?: string }> = React.memo( |
||||
|
({ isHovered, src, ...props }) => ( |
||||
|
<Box |
||||
|
bg="#9985FF" |
||||
|
position="relative" |
||||
|
borderRadius="12px" |
||||
|
mb={space('loose')} |
||||
|
overflow="hidden" |
||||
|
{...props} |
||||
|
> |
||||
|
<Grid style={{ placeItems: 'center' }} height="0px" paddingTop="56.25%"> |
||||
|
<Image |
||||
|
width="102%" |
||||
|
size="102%" |
||||
|
transition={transition('0.45s')} |
||||
|
transform={isHovered && 'scale(1.08)'} |
||||
|
style={{ willChange: 'transform' }} |
||||
|
src={src} |
||||
|
position="absolute" |
||||
|
left={'-2%'} |
||||
|
top={'-2%'} |
||||
|
/> |
||||
|
</Grid> |
||||
|
</Box> |
||||
|
) |
||||
|
); |
@ -0,0 +1,10 @@ |
|||||
|
--- |
||||
|
title: FAQs |
||||
|
description: A knowledge base of question and answers related to the Blockstack ecosystem. |
||||
|
duration: '' |
||||
|
--- |
||||
|
|
||||
|
import { FAQs } from '@components/faq' |
||||
|
export { getStaticProps } from '@common/data/faq' |
||||
|
|
||||
|
<FAQs sections={props.sections} /> |
@ -0,0 +1,144 @@ |
|||||
|
import React from 'react'; |
||||
|
import { Components } from '@components/mdx'; |
||||
|
import { Box, Flex, ChevronIcon, space, color, Grid } from '@blockstack/ui'; |
||||
|
import hydrate from 'next-mdx-remote/hydrate'; |
||||
|
import { Accordion, AccordionItem, AccordionButton, AccordionPanel } from '@reach/accordion'; |
||||
|
import { border } from '@common/utils'; |
||||
|
import { css } from '@styled-system/css'; |
||||
|
import { useRouter } from 'next/router'; |
||||
|
import { useActiveHeading } from '@common/hooks/use-active-heading'; |
||||
|
import { BackButton } from '@components/back-button'; |
||||
|
import Head from 'next/head'; |
||||
|
import { MDContents } from '@components/mdx/md-contents'; |
||||
|
export { getStaticProps, getStaticPaths } from '@common/data/faq'; |
||||
|
import { slugify } from '@common/utils'; |
||||
|
import { PageTop } from '@components/page-top'; |
||||
|
|
||||
|
const getBetterNames = (id: number) => { |
||||
|
switch (id) { |
||||
|
case 360007620914: |
||||
|
return { |
||||
|
title: 'General information', |
||||
|
description: 'General questions about Blockstack and the Stacks network', |
||||
|
img: '/images/pages/testnet.svg', |
||||
|
}; |
||||
|
case 360007411853: |
||||
|
return { |
||||
|
title: 'Stacks Token', |
||||
|
description: 'Questions relating to the native token of Stacks 2.0', |
||||
|
img: '/images/pages/mining.svg', |
||||
|
}; |
||||
|
case 360007760554: |
||||
|
return { |
||||
|
title: 'Stacks blockchain', |
||||
|
description: 'Learn about the blockchain and details of Stacks 2.0', |
||||
|
img: '/images/pages/hello-world.svg', |
||||
|
}; |
||||
|
case 360007781533: |
||||
|
return { |
||||
|
title: 'Ecosystem details', |
||||
|
description: 'Questions related to the age of the project and the contributors', |
||||
|
img: '/images/pages/data-storage.svg', |
||||
|
}; |
||||
|
case 360007780033: |
||||
|
return { |
||||
|
title: 'Building apps', |
||||
|
description: 'Learn about the platform and questions related to decentralized applications', |
||||
|
img: '/images/pages/connect.svg', |
||||
|
}; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const getSlug = (asPath: string) => { |
||||
|
if (asPath.includes('#')) { |
||||
|
const slug = asPath.split('#')[1]; |
||||
|
return slug; |
||||
|
} |
||||
|
return; |
||||
|
}; |
||||
|
|
||||
|
const FAQItem = React.memo(({ faq, ...rest }: any) => { |
||||
|
const id = slugify(faq.title); |
||||
|
const { isActive } = useActiveHeading(id); |
||||
|
|
||||
|
return ( |
||||
|
<Box as={AccordionItem} borderBottom={border()} {...rest}> |
||||
|
<Flex |
||||
|
as={AccordionButton} |
||||
|
_hover={{ color: color('accent') }} |
||||
|
css={css({ |
||||
|
display: 'flex', |
||||
|
width: '100%', |
||||
|
outline: 'none', |
||||
|
alignItems: 'center', |
||||
|
justifyContent: 'space-between', |
||||
|
py: space('extra-loose'), |
||||
|
textAlign: 'left', |
||||
|
color: isActive ? color('accent') : color('text-title'), |
||||
|
':hover': { |
||||
|
color: color('accent'), |
||||
|
}, |
||||
|
})} |
||||
|
> |
||||
|
<Components.h4 my="0px !important" color="currentColor"> |
||||
|
{faq.title} |
||||
|
</Components.h4> |
||||
|
<Box color={color('text-caption')} pl={space('base-loose')}> |
||||
|
<ChevronIcon direction="down" size="22px" /> |
||||
|
</Box> |
||||
|
</Flex> |
||||
|
<Box pb={space('extra-loose')} as={AccordionPanel}> |
||||
|
{hydrate(faq.body, Components)} |
||||
|
</Box> |
||||
|
</Box> |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
const FaqItems = ({ articles }) => { |
||||
|
const router = useRouter(); |
||||
|
const slug = getSlug(router.asPath); |
||||
|
const slugIndex = articles.findIndex(faq => slugify(faq.title) === slug); |
||||
|
const [index, setIndex] = React.useState(slugIndex !== -1 ? slugIndex : 0); |
||||
|
const handleIndexChange = (value: number) => { |
||||
|
setIndex(value); |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Box pr={['0px', 0, 'base-loose']}> |
||||
|
<BackButton href="/references/faqs" mb={0} /> |
||||
|
<Accordion multiple collapsible defaultIndex={index} onChange={handleIndexChange}> |
||||
|
{articles |
||||
|
// @ts-ignore
|
||||
|
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)) |
||||
|
.map((faq, _index) => { |
||||
|
return <FAQItem faq={faq} key={_index} />; |
||||
|
})} |
||||
|
</Accordion> |
||||
|
</Box> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const FaqPage = props => { |
||||
|
const { articles, sections, params } = props; |
||||
|
|
||||
|
const section = sections.find(s => { |
||||
|
const { title } = getBetterNames(s.id); |
||||
|
const slug = slugify(title); |
||||
|
return slug === params.slug; |
||||
|
}); |
||||
|
|
||||
|
const { title, description } = getBetterNames(section.id); |
||||
|
return ( |
||||
|
<> |
||||
|
<Head> |
||||
|
<title>{title} | Blockstack</title> |
||||
|
<meta name="description" content={description} /> |
||||
|
</Head> |
||||
|
<MDContents pageTop={() => <PageTop title={title} description={description} />} headings={[]}> |
||||
|
<FaqItems articles={articles} /> |
||||
|
</MDContents> |
||||
|
</> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default FaqPage; |
Loading…
Reference in new issue