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 FAQ_JSON from '../../_data/faqs.json'; |
|||
import TurndownService from 'turndown'; |
|||
import { slugify } from '@common/utils'; |
|||
import { getBetterNames } from '@common/utils/faqs'; |
|||
|
|||
export const convertFaqAnswersToMDX = async () => { |
|||
const turndownService = new TurndownService(); |
|||
// we convert html to markdown so we can process it with remark,
|
|||
// eg external links open in new window
|
|||
const md = FAQ_JSON.faqs.map(faq => ({ |
|||
...faq, |
|||
answer: turndownService.turndown(faq.answer), |
|||
})); |
|||
// convert it to MDX with next-mdx-remote
|
|||
const answers = await convertRemoteDataToMDX(md, 'answer'); |
|||
const faqs = FAQ_JSON.faqs.map((faq, index) => ({ |
|||
...faq, |
|||
answer: answers[index], |
|||
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 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(); |
|||
// we convert html to markdown so we can process it with remark/rehype,
|
|||
// eg external links open in new window
|
|||
const md = _articles.map(faq => ({ |
|||
...faq, |
|||
body: turndownService.turndown(faq.body), |
|||
})); |
|||
// convert it to MDX with next-mdx-remote
|
|||
const body = await convertRemoteDataToMDX(md, 'body'); |
|||
articles = _articles.map((faq, index) => ({ |
|||
...faq, |
|||
body: body[index], |
|||
})); |
|||
} |
|||
|
|||
return { |
|||
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 { Components } from '@components/mdx'; |
|||
import { Box, Flex, ChevronIcon, space, color } from '@blockstack/ui'; |
|||
import hydrate from 'next-mdx-remote/hydrate'; |
|||
import { Accordion, AccordionItem, AccordionButton, AccordionPanel } from '@reach/accordion'; |
|||
import { border } from '@common/utils'; |
|||
import { Box, space, color, Grid } from '@blockstack/ui'; |
|||
import { Text } from '@components/typography'; |
|||
import { slugify } from '@common/utils'; |
|||
import { css } from '@styled-system/css'; |
|||
import { useRouter } from 'next/router'; |
|||
import { useActiveHeading } from '@common/hooks/use-active-heading'; |
|||
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.question); |
|||
const { isActive } = useActiveHeading(id); |
|||
import { getCapsizeStyles, getHeadingStyles } from '@components/mdx/typography'; |
|||
import { HoverImage } from '../hover-image'; |
|||
import { useTouchable } from '@common/hooks/use-touchable'; |
|||
import Link from 'next/link'; |
|||
import { getBetterNames } from '@common/utils/faqs'; |
|||
|
|||
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 ( |
|||
<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', |
|||
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"> |
|||
{faq.question} |
|||
</Components.h4> |
|||
<Box color={color('text-caption')} pl={space('base-loose')}> |
|||
<ChevronIcon direction="down" size="22px" /> |
|||
<Box |
|||
color={color('text-title')} |
|||
_hover={{ cursor: 'pointer', 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({ |
|||
display: 'block', |
|||
color: color('text-body'), |
|||
mt: space('base-loose'), |
|||
...getCapsizeStyles(16, 26), |
|||
})} |
|||
> |
|||
{description} |
|||
</Text> |
|||
</Box> |
|||
</Flex> |
|||
<Box px={space('extra-loose')} pb={space('extra-loose')} as={AccordionPanel}> |
|||
{hydrate(faq.answer, Components)} |
|||
</Box> |
|||
</Box> |
|||
); |
|||
}); |
|||
export const FAQs = React.memo(({ category, data }: 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); |
|||
}; |
|||
|
|||
}; |
|||
export const FAQs = React.memo(({ articles, sections }: any) => { |
|||
return ( |
|||
<Accordion multiple collapsible defaultIndex={index} onChange={handleIndexChange}> |
|||
{faqs.map((faq, _index) => { |
|||
return <FAQItem faq={faq} key={_index} />; |
|||
<Grid |
|||
gridTemplateColumns="repeat(2, 1fr)" |
|||
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