Merge pull request #15 from lucasPDY/add-faq

Add FAQ Page
This commit is contained in:
Audric Ackermann 2021-05-18 13:17:06 +10:00 committed by GitHub
commit 4224f88569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 139 deletions

View File

@ -148,7 +148,7 @@ body {
&:hover {
filter: brightness(0.9);
&:not(.active) {
color: theme('colors.secondary')
color: theme('colors.secondary');
}
}
@ -169,3 +169,21 @@ body {
ul li > p {
margin-bottom: 0 !important;
}
.accordion {
background-color: theme('colors.secondary');
color: theme('colors.primary');
cursor: pointer;
padding: 0.5em;
width: 100%;
text-align: left;
}
.accordion:hover {
filter: brightness(0.9);
}
.accordion-content {
transition: max-height 0.2s;
overflow: hidden;
}

46
components/Accordion.tsx Normal file
View File

@ -0,0 +1,46 @@
import React, { useState, useRef } from 'react';
import { RichBody } from '../components/RichBody';
import classNames from 'classnames';
import TriangleSVG from '../assets/svgs/triangle.svg';
export function Accordion(props) {
const { question, answer } = props;
const [isActive, setActiveState] = useState(false);
const [height, setHeight] = useState('0px');
const content = useRef(null);
function toggleAccordion() {
setActiveState(!isActive);
setHeight(isActive ? '0px' : `${content.current.scrollHeight}px`);
}
return (
<div className="mb-1 border border-current rounded-sm">
<button
className={
'flex accordion text-xl tablet:text-2xl w-full text-left bg-secondary px-3 justify-between items-center'
}
onClick={toggleAccordion}
>
<div style={{ maxWidth: '95%' }}> {question}</div>
<TriangleSVG
className={classNames(
'h-3 fill-current text-primary transform outline-none cursor-pointer duration-200',
isActive ? 'rotate-90' : '',
)}
/>{' '}
</button>
<div
ref={content}
style={{
maxHeight: height,
}}
className={classNames('accordion-content')}
>
<div className="w-full px-4 pt-4 text-lg text-left ease-in-out tablet:text-xl">
<RichBody body={answer} />
</div>
</div>
</div>
);
}

View File

@ -13,6 +13,9 @@ const METADATA = {
DESCRIPTION: "View Oxen's Blog Updates Here",
URL: 'https://oxen.io/blog',
},
FAQ: {
DESCRIPTION: 'View Some Frequently Asked Questions here',
},
};
export default METADATA;

View File

@ -62,6 +62,11 @@ const SIDE_MENU_ITEMS = {
label: "Oxen's 2021 roadmap",
href: '/roadmap',
},
[SideMenuItem.FAQ]: {
id: 9,
label: 'Frequently Asked Questions',
href: '/faq',
},
} as { [name: string]: ISideMenuItem };
const MENU_ITEMS: IMenuItem[] = [

View File

@ -4,14 +4,14 @@ import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Contained } from '../components/Contained';
import { RichBody } from '../components/RichBody';
import { NAVIGATION, METADATA } from '../constants';
import { NAVIGATION } from '../constants';
import { CmsApi, unslugify } from '../services/cms';
import { PageType, setPageType, SideMenuItem } from '../state/navigation';
import { ISplitPage } from '../types/cms';
import { generateTitle, generateURL } from '../utils/metadata';
interface IPath {
params: { page: string; isRoadmap?: boolean };
params: { page: string };
}
export async function getStaticPaths() {
@ -31,17 +31,6 @@ export async function getStaticPaths() {
export async function getStaticProps({ params }) {
const href = params?.page ?? '';
const id = unslugify(String(href));
// Roadmap page is special 👁️👄👁️
if (SideMenuItem[id] == [SideMenuItem.ROADMAP]) {
return {
props: {
page: null,
isRoadmap: true,
href: `/${href}`, // the '/' is removed from the href from getStaticPaths(), so let's add it back here
},
revalidate: 60,
};
}
const cms = new CmsApi();
const page = await cms.fetchPageById(SideMenuItem[id]);
@ -52,39 +41,20 @@ export async function getStaticProps({ params }) {
return {
props: {
page,
isRoadmap: false,
href: `/${href}`,
},
revalidate: 60,
};
}
function Page({
page,
isRoadmap,
href,
}: {
page: ISplitPage | null;
isRoadmap?: boolean;
href: string;
}) {
function Page({ page, href }: { page: ISplitPage | null; href: string }) {
const dispatch = useDispatch();
useEffect(() => {
dispatch(setPageType(PageType.NORMAL));
}, []);
const pageTitle = generateTitle(
isRoadmap
? NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.ROADMAP].label
: page?.label,
);
const pageDescription = isRoadmap
? METADATA.ROADMAP.DESCRIPTION
: page?.title;
const pageURL = generateURL(
isRoadmap ? NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.ROADMAP].href : href,
);
const pageTitle = generateTitle(page?.label);
const pageDescription = page?.title;
const pageURL = generateURL(href);
return (
<>

98
pages/faq.tsx Normal file
View File

@ -0,0 +1,98 @@
import Head from 'next/head';
import React from 'react';
import { GetServerSideProps } from 'next';
import { NAVIGATION, METADATA, CMS } from '../constants';
import { SideMenuItem } from '../state/navigation';
import { generateTitle, generateURL } from '../utils/metadata';
import { CmsApi } from '../services/cms';
import { IFAQItem } from '../types/cms';
import { Accordion } from '../components/Accordion';
import { Contained } from '../components/Contained';
export const getServerSideProps: GetServerSideProps = async context => {
const cms = new CmsApi();
// Fetch posts even when tag, for related etc
// Pagination only occurs when tag isnt defined.
// If tag is defined, pagination is for tag results
const { faqItems, total } = await cms.fetchFAQItems();
return {
props: {
faqItems,
total,
},
};
};
interface Props {
faqItems: IFAQItem[];
total: number;
}
function FAQ(props: Props) {
const { faqItems } = props;
const pageTitle = generateTitle(
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.FAQ].label,
);
const pageURL = generateURL(
NAVIGATION.SIDE_MENU_ITEMS[SideMenuItem.FAQ].href,
);
const imagePathLocal = 'img/what_is_oxen.png';
const imageURL = `${METADATA.OXEN_HOST_URL}/${imagePathLocal}`;
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={METADATA.FAQ.DESCRIPTION}></meta>
<meta property="og:title" content={pageTitle} key="ogtitle" />
<meta
property="og:description"
content={METADATA.FAQ.DESCRIPTION}
key="ogdesc"
/>
<meta property="og:type" content="website" />
<meta property="og:image" content={imageURL} key="ogimage" />
<meta property="og:url" content={pageURL} />
<link rel="canonical" href={pageURL}></link>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={METADATA.FAQ.DESCRIPTION} />
<meta name="twitter:image" content={imageURL} />
</Head>
<div className="bg-alt">
<div className="relative flex items-center justify-center w-full h-full pt-3 bg-gradient-to-bl from-hyper to-blush">
<img
style={{ maxHeight: '33vh' }}
src={imagePathLocal}
className="object-contain w-full"
/>
</div>
<Contained>
<div className="flex flex-col pb-6 mx-4">
<div>
<h1 className="mt-6 mb-3 text-3xl font-medium text-left tablet:text-4xl font-prompt text-primary">
FAQ
</h1>
</div>
{faqItems.map(faqItem => (
<div key={faqItem.id}>
<Accordion
question={faqItem.question}
answer={faqItem.answer}
/>
</div>
))}
</div>
</Contained>
</div>
</>
);
}
export default FAQ;

View File

@ -39,7 +39,7 @@ function Roadmap() {
/>
<meta property="og:type" content="website" />
<meta property="og:url" content={pageURL} />
<meta property="og:image" content={imageURL} key="ogimage" />
<link rel="canonical" href={pageURL}></link>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />

BIN
public/img/what_is_oxen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -9,13 +9,24 @@ import TelegramSVG from '../assets/svgs/socials/brand-telegram.svg';
import { Button } from '../components/Button';
import { CMS } from '../constants';
import { SideMenuItem, TPages } from '../state/navigation';
import { IAuthor, IFigureImage, IPost, ISplitPage } from '../types/cms';
import {
IAuthor,
IFigureImage,
IPost,
ISplitPage,
IFAQItem,
} from '../types/cms';
interface IFetchBlogEntriesReturn {
posts: Array<IPost>;
total: number;
}
interface IFetchFAQItemsReturn {
faqItems: Array<IFAQItem>;
total: number;
}
// Turns CMS IDs into slugs
export const slugify = (id: string) => id?.replace(/_/g, '-').toLowerCase();
export const unslugify = (slug: string) =>
@ -155,6 +166,20 @@ export class CmsApi {
});
}
public async fetchFAQItems(): Promise<IFetchFAQItemsReturn> {
const entries = await this.client.getEntries({
content_type: 'faq_item', // only fetch faq items
order: 'fields.id',
});
if (entries && entries.items && entries.items.length > 0) {
const faqItems = entries.items.map(entry => this.convertFAQ(entry));
return { faqItems, total: entries.total };
}
return { faqItems: [], total: 0 } as IFetchFAQItemsReturn;
}
public convertImage = (rawImage): IFigureImage =>
rawImage
? {
@ -211,6 +236,17 @@ export class CmsApi {
hero: this.convertImage(rawHero),
};
};
public convertFAQ = (rawData): IFAQItem => {
const rawFAQ = rawData.fields;
const { question, answer, id } = rawFAQ;
return {
id: id ?? null,
question: question ?? null,
answer: answer ?? null,
};
};
}
const extractShortcodeGeneralButton = (shortcode: string) => {

View File

@ -1,100 +0,0 @@
'use strict';
exports.__esModule = true;
exports.collapseMobileHeader = exports.expandMobileHeaderMenu = exports.setSplitPagesContent = exports.collapseSideMenu = exports.expandSideMenu = exports.setPostTitle = exports.setPageType = exports.setHeaderCollapsed = exports.NavigationActions = exports.initialNavigationState = exports.PageType = exports.SideMenuItem = void 0;
var SideMenuItem;
(function (SideMenuItem) {
SideMenuItem['BUILD'] = 'BUILD';
SideMenuItem['BUY_OXEN'] = 'BUY_OXEN';
SideMenuItem['WHO_ARE_WE'] = 'WHO_ARE_WE';
SideMenuItem['STAKE'] = 'STAKE';
SideMenuItem['USES'] = 'USES';
SideMenuItem['SESSION_LOKINET'] = 'SESSION_LOKINET';
SideMenuItem['GET_INVOLVED'] = 'GET_INVOLVED';
SideMenuItem['ROADMAP'] = 'ROADMAP';
})((SideMenuItem = exports.SideMenuItem || (exports.SideMenuItem = {})));
var PageType;
(function (PageType) {
PageType['NORMAL'] = 'NORMAL';
PageType['BLOG'] = 'BLOG';
PageType['POST'] = 'POST';
})((PageType = exports.PageType || (exports.PageType = {})));
exports.initialNavigationState = {
// Side menu expanded only toggles for mobile.
// On desktop it's always open (if it fits).
pageType: PageType.NORMAL,
postTitle: undefined,
headerCollapsed: true,
sideMenuExpanded: false,
headerMobileMenuExpanded: false,
};
var NavigationActions;
(function (NavigationActions) {
NavigationActions['SET_HEADER_COLLAPSED'] = 'SET_HEADER_COLLAPSED';
NavigationActions['SET_PAGE_TYPE'] = 'SET_PAGE_TYPE';
NavigationActions['SET_POST_TITLE'] = 'SET_POST_TITLE';
NavigationActions['EXPAND_SIDE_MENU'] = 'EXPAND_SIDE_MENU';
NavigationActions['COLLAPSE_SIDE_MENU'] = 'COLLAPSE_SIDE_MENU';
NavigationActions['SET_SPLIT_PAGES_CONTENT'] = 'SET_SPLIT_PAGES_CONTENT';
NavigationActions['EXPAND_MOBILE_HEADER_MENU'] = 'EXPAND_MOBILE_HEADER_MENU';
NavigationActions['COLLAPSE_MOBILE_HEADER_MENU'] =
'COLLAPSE_MOBILE_HEADER_MENU';
})(
(NavigationActions =
exports.NavigationActions || (exports.NavigationActions = {})),
);
// ////////////////////////////// //
// Action Creators //
// ////////////////////////////// //
var setHeaderCollapsed = function (collapsed) {
return {
type: NavigationActions.SET_HEADER_COLLAPSED,
payload: collapsed,
};
};
exports.setHeaderCollapsed = setHeaderCollapsed;
var setPageType = function (type) {
return {
type: NavigationActions.SET_PAGE_TYPE,
payload: type,
};
};
exports.setPageType = setPageType;
// For sidebar title
var setPostTitle = function (title) {
return {
type: NavigationActions.SET_POST_TITLE,
payload: title,
};
};
exports.setPostTitle = setPostTitle;
var expandSideMenu = function () {
return {
type: NavigationActions.EXPAND_SIDE_MENU,
};
};
exports.expandSideMenu = expandSideMenu;
var collapseSideMenu = function () {
return {
type: NavigationActions.COLLAPSE_SIDE_MENU,
};
};
exports.collapseSideMenu = collapseSideMenu;
var setSplitPagesContent = function (pages) {
return {
type: NavigationActions.SET_SPLIT_PAGES_CONTENT,
payload: pages,
};
};
exports.setSplitPagesContent = setSplitPagesContent;
var expandMobileHeaderMenu = function () {
return {
type: NavigationActions.EXPAND_MOBILE_HEADER_MENU,
};
};
exports.expandMobileHeaderMenu = expandMobileHeaderMenu;
var collapseMobileHeader = function () {
return {
type: NavigationActions.COLLAPSE_MOBILE_HEADER_MENU,
};
};
exports.collapseMobileHeader = collapseMobileHeader;

View File

@ -10,6 +10,7 @@ export enum SideMenuItem {
GET_INVOLVED = 'GET_INVOLVED',
ROADMAP = 'ROADMAP',
AIRDROP = 'AIRDROP',
FAQ = 'FAQ',
}
export type TPages = {

View File

@ -44,3 +44,9 @@ export interface ISplitPage {
body: Document;
hero?: IFigureImage;
}
export interface IFAQItem {
id: number;
question: string;
answer: Document;
}