Make Q&A its own section on posthog.com (#3007)

* Make Q&A its own section on posthog.com

* red links

* add appears on

* facet filter
This commit is contained in:
Eli Kinsey
2022-02-22 08:08:19 -08:00
committed by GitHub
parent 9c94c78ae7
commit cacf3003c7
10 changed files with 368 additions and 25 deletions

View File

@@ -15,7 +15,9 @@ export const onRouteUpdate = ({ location, prevLocation }) => {
// Checking for prevLocation prevents this from happening twice
if (typeof window !== 'undefined' && prevLocation) {
var slug = location.pathname.substring(1)
var theme = /^handbook|^docs|^blog|^integrations|^product|^tutorials/.test(slug) ? window.__theme : 'light'
var theme = /^handbook|^docs|^blog|^integrations|^product|^tutorials|^questions/.test(slug)
? window.__theme
: 'light'
document.body.className = theme
}
}

View File

@@ -36,7 +36,7 @@ export const onRenderBody = function ({ setPreBodyComponents }) {
})
try {
preferredTheme =
(/^handbook|^docs|^blog|^integrations|^product|^tutorials/.test(slug) &&
(/^handbook|^docs|^blog|^integrations|^product|^tutorials|^questions/.test(slug) &&
(localStorage.getItem('theme') || (darkQuery.matches ? 'dark' : 'light'))) ||
'light'
} catch (err) {}

View File

@@ -17,6 +17,7 @@ module.exports = exports.createPages = async ({ actions, graphql }) => {
const TutorialsCategoryTemplate = path.resolve(`src/templates/TutorialsCategory.js`)
const TutorialsAuthorTemplate = path.resolve(`src/templates/TutorialsAuthor.js`)
const HostHogTemplate = path.resolve(`src/templates/HostHog.js`)
const Question = path.resolve(`src/templates/Question.js`)
const result = await graphql(`
{
allMdx(
@@ -178,6 +179,11 @@ module.exports = exports.createPages = async ({ actions, graphql }) => {
slug
}
}
questions: allQuestion {
nodes {
id
}
}
}
`)
@@ -416,4 +422,14 @@ module.exports = exports.createPages = async ({ actions, graphql }) => {
})
}
})
result.data.questions.nodes.forEach((node) => {
const { id } = node
createPage({
path: `questions/${id}`,
component: Question,
context: {
id,
},
})
})
}

View File

@@ -181,6 +181,7 @@ module.exports = exports.sourceNodes = async ({ actions, createContentDigest, cr
name,
imageURL,
fullName,
ts: new Date(ts * 1000),
}
createNode(replyNode)
createParentChildLink({ parent: node, child: replyNode })

View File

@@ -2,12 +2,25 @@ import { MDXProvider } from '@mdx-js/react'
import { Blockquote } from 'components/BlockQuote'
import { CodeBlock } from 'components/CodeBlock'
import { InlineCode } from 'components/InlineCode'
import Link from 'components/Link'
import { ZoomImage } from 'components/ZoomImage'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import React from 'react'
import Avatar from './Avatar'
const Reply = ({ avatar, name, childMdx, teamMember }) => {
export const Days = ({ ts, url }) => {
const days = Number(ts)
return (
<Link
to={url}
className="font-normal ml-2 text-black hover:text-black dark:text-white dark:hover:text-white opacity-30"
>
{ts} day{days === 1 ? '' : 's'} ago
</Link>
)
}
export const Reply = ({ avatar, name, childMdx, teamMember, ts, parentId, id, className = '' }) => {
const components = {
inlineCode: InlineCode,
blockquote: Blockquote,
@@ -15,31 +28,34 @@ const Reply = ({ avatar, name, childMdx, teamMember }) => {
img: ZoomImage,
}
return (
<div className="bg-gray-accent-light dark:bg-gray-accent-dark p-4 rounded-md w-full mt-3">
<div className="flex space-x-2 items-center">
<div id={id} className={`py-4 rounded-md w-full mt-3 ${className}`}>
<div className="flex space-x-4 items-start">
<Avatar image={teamMember?.frontmatter?.headshot || avatar} />
<p className="m-0 text-[14px] font-semibold">
<span>{teamMember?.frontmatter?.name || name}</span>
<span className="opacity-50">, {teamMember?.frontmatter?.jobTitle || 'Contributor'}</span>
</p>
</div>
<div className="my-3">
<MDXProvider components={components}>
<MDXRenderer>{childMdx.body}</MDXRenderer>
</MDXProvider>
<div className="min-w-0">
<p className="m-0 text-[14px] font-semibold text-black dark:text-white">
<span>{teamMember?.frontmatter?.name || name}</span>
<span className="opacity-50">, {teamMember?.frontmatter?.jobTitle || 'Contributor'}</span>
<Days ts={ts} url={`/questions/${parentId}#${id}`} />
</p>
<div className="my-3">
<MDXProvider components={components}>
<MDXRenderer>{childMdx.body}</MDXRenderer>
</MDXProvider>
</div>
</div>
</div>
</div>
)
}
export default function Question({ question }) {
export default function Question({ question, id }) {
const components = {
inlineCode: InlineCode,
blockquote: Blockquote,
pre: CodeBlock,
img: ZoomImage,
}
const { avatar, childMdx, name } = question[0]
const { avatar, childMdx, name, ts } = question[0]
return (
<div className="flex items-start space-x-4 w-full">
<Avatar image={avatar} />
@@ -49,8 +65,12 @@ export default function Question({ question }) {
<MDXRenderer>{childMdx.body}</MDXRenderer>
</MDXProvider>
</div>
<p className="text-[14px] font-semibold opacity-50 mb-3">by {name}</p>
{question.length > 1 && question.slice(1).map((reply, index) => <Reply key={index} {...reply} />)}
<p className="text-[14px] mb-3 text-black dark:text-white">
<span className="font-semibold opacity-50">by {name}</span>{' '}
<Days ts={ts} url={`/questions/${id}`} />
</p>
{question.length > 1 &&
question.slice(1).map((reply, index) => <Reply key={index} parentId={id} {...reply} />)}
</div>
</div>
)

View File

@@ -13,7 +13,7 @@ export default function CommunityQuestions({ questions }) {
return (
question.childrenReply &&
question.childrenReply.length > 0 && (
<Question key={index} question={question.childrenReply} />
<Question id={question.id} key={index} question={question.childrenReply} />
)
)
})}

View File

@@ -42,7 +42,7 @@ const A = (props) => <Link {...props} className="text-red hover:text-red font-se
export const SidebarSection = ({ title, children, className = '' }) => {
return (
<div className={`py-4 ${className}`}>
<div className={`py-4 px-5 lg:px-8 ${className}`}>
{title && <h3 className="text-[13px] opacity-40 font-semibold mb-3">{title}</h3>}
{children}
</div>
@@ -97,7 +97,20 @@ export const Contributor = ({ image, name }) => {
return (
<>
<div className="w-[38px] h-[38px] relative rounded-full overflow-hidden">
<GatsbyImage image={getImage(image)} />
{image ? (
<GatsbyImage image={getImage(image)} />
) : (
<svg width="38" height="38" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.0782 41.0392H5.42978C4.03134 41.0392 3.1173 40.1642 3.09386 38.7736C3.07823 37.7814 3.07042 36.797 3.10948 35.8048C3.15636 34.6329 3.72668 33.7345 4.74228 33.1798C8.0782 31.3595 11.4299 29.5783 14.7659 27.7658C15.0081 27.633 15.1565 27.758 15.3362 27.8517C18.1878 29.3439 21.0942 29.4689 24.0626 28.2267C24.1485 28.1955 24.2423 28.1721 24.3126 28.1096C24.9298 27.5861 25.4845 27.7971 26.1251 28.1486C29.1173 29.7971 32.1331 31.4143 35.1487 33.0238C36.4534 33.7191 37.094 34.766 37.0706 36.2426C37.0549 37.0785 37.0706 37.9067 37.0706 38.7426C37.0628 40.1254 36.1409 41.0395 34.7659 41.0395H20.0783L20.0782 41.0392Z"
fill="#BFBFBC"
/>
<path
d="M19.8359 27.0625C17.0859 26.9687 14.8047 25.6094 13.1251 23.1953C10.3751 19.2344 10.7032 13.6093 13.8516 10.0001C17.2735 6.08599 22.9452 6.10943 26.336 10.0469C29.9376 14.2345 29.711 20.8437 25.8126 24.6405C24.2188 26.1952 22.3126 27.0312 19.8362 27.0624L19.8359 27.0625Z"
fill="#BFBFBC"
/>
</svg>
)}
</div>
<span className="author text-[14px] font-semibold">{name}</span>
</>
@@ -131,7 +144,14 @@ export const Text = ({ children }) => {
return <p className="m-0 opacity-50 font-semibold flex items-center space-x-2 text-[14px]">{children}</p>
}
export default function PostLayout({ tableOfContents, children, sidebar, contentWidth = 650, questions }) {
export default function PostLayout({
tableOfContents,
children,
sidebar,
contentWidth = 650,
questions,
article = true,
}) {
const { hash } = useLocation()
const breakpoints = useBreakpoint()
const [view, setView] = useState('Article')
@@ -150,16 +170,16 @@ export default function PostLayout({ tableOfContents, children, sidebar, content
className="w-full relative lg:grid lg:grid-flow-col items-start -mb-20"
>
<article className="col-span-2 px-5 lg:px-8 lg:border-r border-dashed border-gray-accent-light dark:border-gray-accent-dark mt-10 lg:mt-0 lg:pt-10 lg:pb-20 ml-auto">
<div style={{ maxWidth: contentWidth }} className="w-full article-content">
<div style={{ maxWidth: contentWidth }} className={`w-full ${article ? 'article-content' : ''}`}>
{children}
</div>
{questions && questions}
</article>
<aside className="lg:sticky top-10 flex-shrink-0 w-full lg:w-[229px] justify-self-end px-5 lg:px-8 lg:box-content my-10 lg:my-0 lg:mt-10 pb-20 mr-auto overflow-y-auto lg:h-[calc(100vh-7.5rem)]">
<aside className="lg:sticky top-10 flex-shrink-0 w-full justify-self-end lg:box-content my-10 lg:my-0 lg:mt-10 pb-20 mr-auto overflow-y-auto lg:h-[calc(100vh-7.5rem)]">
<div className="grid divide-y divide-gray-accent-light dark:divide-gray-accent-dark divide-dashed">
{sidebar && sidebar}
{view === 'Article' && !breakpoints.md && toc?.length > 1 && (
<div className="pt-12 !border-t-0">
<div className="pt-12 px-5 lg:px-8 !border-t-0">
<h4 className="text-[13px] mb-2">On this page</h4>
<Scrollspy
offset={-50}

160
src/pages/questions.js Normal file
View File

@@ -0,0 +1,160 @@
import { DocSearchModal } from '@docsearch/react'
import { Blockquote } from 'components/BlockQuote'
import Breadcrumbs from 'components/Breadcrumbs'
import { CodeBlock } from 'components/CodeBlock'
import { Days } from 'components/CommunityQuestions/Question'
import { InlineCode } from 'components/InlineCode'
import Layout from 'components/Layout'
import Link from 'components/Link'
import { SEO } from 'components/seo'
import { ZoomImage } from 'components/ZoomImage'
import { graphql } from 'gatsby'
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import React, { useState } from 'react'
const Search = () => {
const [value, setValue] = useState('')
const [modal, setModal] = useState(false)
const handleSubmit = (e) => {
e.preventDefault()
if (value.trim()) {
setModal(true)
}
}
return (
<>
{modal && (
<DocSearchModal
searchParameters={{ facetFilters: [`tags:questions`] }}
onClose={() => setModal(false)}
initialQuery={value}
appId="B763I3AO0D"
indexName="posthog"
apiKey="f1386529b9fafc5c3467e0380f19de4b"
/>
)}
<form onSubmit={handleSubmit} className="flex space-x-3 m-0">
<input
onChange={(e) => setValue(e.target.value)}
value={value}
name="faq-search"
placeholder="Search hundreds of FAQs..."
className="px-4 py-3 bg-white dark:bg-gray-accent-dark shadow-md rounded-md max-w-[477px] w-full"
/>
<button className="px-6 py-3 bg-red shadow-md rounded-md text-white font-bold">Search</button>
</form>
<p className="text-[13px] opacity-50 m-0 mx-4 mt-3">
Try product questions, or anything about installation or self-hosting.
</p>
</>
)
}
const Question = ({ question }) => {
const replies = question.childrenReply
const { avatar, name, ts, childMdx } = replies[0]
const components = {
inlineCode: InlineCode,
blockquote: Blockquote,
pre: CodeBlock,
img: ZoomImage,
}
return (
<li className="mt-9">
<div className="flex space-x-2">
{avatar ? (
<GatsbyImage className="rounded-full overflow-hidden" image={getImage(avatar)} />
) : (
<svg width="20" height="20" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.0782 41.0392H5.42978C4.03134 41.0392 3.1173 40.1642 3.09386 38.7736C3.07823 37.7814 3.07042 36.797 3.10948 35.8048C3.15636 34.6329 3.72668 33.7345 4.74228 33.1798C8.0782 31.3595 11.4299 29.5783 14.7659 27.7658C15.0081 27.633 15.1565 27.758 15.3362 27.8517C18.1878 29.3439 21.0942 29.4689 24.0626 28.2267C24.1485 28.1955 24.2423 28.1721 24.3126 28.1096C24.9298 27.5861 25.4845 27.7971 26.1251 28.1486C29.1173 29.7971 32.1331 31.4143 35.1487 33.0238C36.4534 33.7191 37.094 34.766 37.0706 36.2426C37.0549 37.0785 37.0706 37.9067 37.0706 38.7426C37.0628 40.1254 36.1409 41.0395 34.7659 41.0395H20.0783L20.0782 41.0392Z"
fill="#BFBFBC"
/>
<path
d="M19.8359 27.0625C17.0859 26.9687 14.8047 25.6094 13.1251 23.1953C10.3751 19.2344 10.7032 13.6093 13.8516 10.0001C17.2735 6.08599 22.9452 6.10943 26.336 10.0469C29.9376 14.2345 29.711 20.8437 25.8126 24.6405C24.2188 26.1952 22.3126 27.0312 19.8362 27.0624L19.8359 27.0625Z"
fill="#BFBFBC"
/>
</svg>
)}
<p className="text-black dark:text-white m-0 text-[13px] font-semibold opacity-50 flex space-x-2">
{name} <Days ts={ts} url={`/questions/${question.id}`} />
</p>
</div>
<div className="artcle-content my-2">
<p className="m-0">{childMdx.excerpt}</p>
</div>
<Link className="text-red hover:red m-0 text-[13px] font-semibold" to={`/questions/${question.id}`}>
{replies.length - 1} response{replies.length - 1 === 1 ? '' : 's'}
</Link>
</li>
)
}
export default function FAQ({
data: {
questions: { nodes },
},
}) {
return (
<Layout>
<SEO title={'Questions - PostHog'} />
<Breadcrumbs
crumbs={[{ title: 'Questions' }]}
darkModeToggle
className="px-4 mt-4 sticky top-[-2px] z-10 bg-tan dark:bg-primary"
/>
<section className="max-w-[884px] mx-auto my-12 px-5">
<h1>FAQ</h1>
<p>
Weve been asked hundreds of questions, so we decided to compile the answers here. (We hope this
helps you find what youre looking for even faster!)
</p>
<Search />
<div className="questions-content">
<ul className="list-none p-0 m-0">
{nodes.map((question, index) => (
<Question key={index} question={question} />
))}
</ul>
</div>
</section>
</Layout>
)
}
export const query = graphql`
query AllQuestionsQuery {
questions: allQuestion(sort: { fields: childReply___ts, order: DESC }, limit: 20) {
nodes {
id
childrenReply {
id
name
ts(difference: "days")
childMdx {
body
excerpt(pruneLength: 400)
}
avatar {
childImageSharp {
gatsbyImageData(width: 20, height: 20)
}
}
teamMember {
frontmatter {
name
jobTitle
headshot {
childImageSharp {
gatsbyImageData(width: 20, height: 20)
}
}
}
}
}
}
}
}
`

View File

@@ -145,8 +145,11 @@ export const query = graphql`
}
questions: allQuestion(filter: { slug: { in: [$slug] } }) {
nodes {
id
childrenReply {
id
name
ts(difference: "days")
childMdx {
body
}

121
src/templates/Question.js Normal file
View File

@@ -0,0 +1,121 @@
import { MDXProvider } from '@mdx-js/react'
import { Blockquote } from 'components/BlockQuote'
import Breadcrumbs from 'components/Breadcrumbs'
import { Reply } from 'components/CommunityQuestions/Question'
import { InlineCode } from 'components/InlineCode'
import Layout from 'components/Layout'
import Link from 'components/Link'
import PostLayout, { Contributors, SidebarSection, Text } from 'components/PostLayout'
import { SEO } from 'components/seo'
import { ZoomImage } from 'components/ZoomImage'
import { graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import React from 'react'
import { CodeBlock } from '../components/CodeBlock'
const QuestionSidebar = ({ question, location, title, pageViews, categories, slugs }) => {
const days = Number(question.ts)
return (
<>
<SidebarSection title={`Asked by`}>
<Contributors
className="flex flex-col space-y-2"
contributors={[{ name: question.name, image: question.avatar }]}
/>
</SidebarSection>
<SidebarSection>
<Text>
Posted {days} day{days === 1 ? '' : 's'} ago
</Text>
</SidebarSection>
{slugs && (
<SidebarSection title={`Appears on`}>
<ul className="list-none m-0 p-0 flex flex-col space-y-2">
{slugs.map((slug) => {
return (
<li key={slug}>
<Link to={slug}>{slug}</Link>
</li>
)
})}
</ul>
</SidebarSection>
)}
</>
)
}
export default function Question({
data: {
question: { childrenReply, id, slug },
},
}) {
const question = childrenReply[0]
const components = {
inlineCode: InlineCode,
blockquote: Blockquote,
pre: CodeBlock,
img: ZoomImage,
}
return (
<Layout>
<SEO title={'Questions - PostHog'} />
<Breadcrumbs
crumbs={[
{ title: 'Questions', url: '/questions' },
{ title: `${question.name}'s question`, truncate: true },
]}
darkModeToggle
className="px-4 mt-4 sticky top-[-2px] z-10 bg-tan dark:bg-primary"
/>
<PostLayout article={false} sidebar={<QuestionSidebar slugs={slug} question={question} />}>
<div className="bg-white dark:bg-gray-accent-dark p-5 rounded-md shadow-lg article-content questions-content">
<MDXProvider components={components}>
<MDXRenderer>{question.childMdx.body}</MDXRenderer>
</MDXProvider>
</div>
<div className="mt-6">
{childrenReply.length > 1 &&
childrenReply
.slice(1)
.map((reply, index) => (
<Reply className="!bg-transparent !py-2" parentId={id} key={index} {...reply} />
))}
</div>
</PostLayout>
</Layout>
)
}
export const query = graphql`
query QuestionQuery($id: String!) {
question(id: { eq: $id }) {
id
slug
childrenReply {
id
name
ts(difference: "days")
childMdx {
body
}
avatar {
childImageSharp {
gatsbyImageData(width: 37, height: 37)
}
}
teamMember {
frontmatter {
name
jobTitle
headshot {
childImageSharp {
gatsbyImageData(width: 37, height: 37)
}
}
}
}
}
}
}
`