oxen-website/services/cms.tsx

558 lines
15 KiB
TypeScript
Raw Normal View History

import { Document, Block, Inline } from '@contentful/rich-text-types';
2021-09-03 15:21:12 +02:00
import { ContentfulClientApi, createClient, EntryCollection } from 'contentful';
2021-09-02 15:59:59 +02:00
import { format, parseISO } from 'date-fns';
2021-02-15 04:22:26 +01:00
import React from 'react';
2021-02-25 05:33:47 +01:00
import BittrexSVG from '../assets/svgs/bittrex-logo.svg';
import KucoinSVG from '../assets/svgs/kucoin-logo.svg';
2021-02-18 04:07:53 +01:00
import DiscordSVG from '../assets/svgs/socials/brand-discord.svg';
import RedditSVG from '../assets/svgs/socials/brand-reddit.svg';
import TelegramSVG from '../assets/svgs/socials/brand-telegram.svg';
2021-02-15 04:22:26 +01:00
import { Button } from '../components/Button';
import EmailSignup from '../components/EmailSignup';
2021-02-12 06:37:44 +01:00
import { CMS } from '../constants';
2021-02-11 01:38:40 +01:00
import { SideMenuItem, TPages } from '../state/navigation';
2021-05-10 07:36:02 +02:00
import {
IAuthor,
IFigureImage,
IPost,
ISplitPage,
IFAQItem,
2021-09-03 15:21:12 +02:00
IFetchBlogEntriesReturn,
IFetchEntriesReturn,
IFetchFAQItemsReturn,
2021-09-15 06:08:23 +02:00
ITagList,
2021-05-10 07:36:02 +02:00
} from '../types/cms';
2021-07-30 10:27:47 +02:00
import isLive from '../utils/environment';
2021-09-19 04:27:09 +02:00
import { generateURL } from '../constants/metadata';
import { fetchContent } from './embed';
2021-01-29 03:50:49 +01:00
2021-07-30 10:27:47 +02:00
function loadOptions(options: any) {
if (isLive()) options['fields.live'] = true;
return options;
}
2021-02-10 09:38:27 +01:00
// Turns CMS IDs into slugs
export const slugify = (id: string) => id?.replace(/_/g, '-').toLowerCase();
export const unslugify = (slug: string) =>
slug.replace(/-/g, '_').toUpperCase();
2021-02-04 06:42:15 +01:00
export class CmsApi {
2021-01-29 03:50:49 +01:00
client: ContentfulClientApi;
constructor() {
this.client = createClient({
space: process.env.CONTENTFUL_SPACE_ID,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});
}
2021-09-15 06:08:23 +02:00
public async fetchTagList(): Promise<ITagList> {
// TODO Migrate to Contentful Tag System
const { entries, total } = await this.fetchBlogEntries();
const tags: ITagList = {};
entries.forEach(entry => {
entry.tags.forEach(tag => {
if (!tags[tag]) {
tags[tag] = tag;
}
});
});
return tags;
}
2021-02-18 04:07:53 +01:00
public async fetchBlogEntries(
2021-04-23 08:30:52 +02:00
quantity = CMS.BLOG_RESULTS_PER_PAGE,
2021-02-18 04:07:53 +01:00
page = 1,
): Promise<IFetchBlogEntriesReturn> {
2021-09-03 15:21:12 +02:00
const _entries = await this.client.getEntries(
2021-07-30 10:27:47 +02:00
loadOptions({
content_type: 'post', // only fetch blog post entry
order: '-fields.date',
limit: quantity,
skip: (page - 1) * quantity,
}),
);
2021-02-18 04:07:53 +01:00
2021-09-03 15:21:12 +02:00
const results = await this.generateEntries(_entries, 'post');
return {
entries: results.entries as Array<IPost>,
total: results.total,
};
2021-02-04 06:42:15 +01:00
}
2021-02-23 08:20:00 +01:00
public async fetchBlogEntriesByTag(
tag: string,
2021-04-23 08:30:52 +02:00
quantity = CMS.BLOG_RESULTS_PER_PAGE_TAGGED,
2021-02-23 08:20:00 +01:00
page = 1,
): Promise<IFetchBlogEntriesReturn> {
2021-09-03 15:21:12 +02:00
const _entries = await this.client.getEntries(
2021-07-30 10:27:47 +02:00
loadOptions({
content_type: 'post',
order: '-fields.date',
'fields.tags[in]': tag,
limit: quantity,
skip: (page - 1) * quantity,
}),
);
2021-02-23 07:48:23 +01:00
2021-09-03 15:21:12 +02:00
const results = await this.generateEntries(_entries, 'post');
return {
entries: results.entries as Array<IPost>,
total: results.total,
};
2021-02-11 06:26:56 +01:00
}
public async fetchBlogEntriesWithoutDevUpdates(
2021-04-23 08:30:52 +02:00
quantity = CMS.BLOG_RESULTS_PER_PAGE,
page = 1,
): Promise<IFetchBlogEntriesReturn> {
const DEV_UPDATE_TAG = 'dev-update';
2021-09-03 15:21:12 +02:00
const _entries = await this.client.getEntries(
2021-07-30 10:27:47 +02:00
loadOptions({
content_type: 'post', // only fetch blog post entry
order: '-fields.date',
'fields.tags[ne]': DEV_UPDATE_TAG, // Exclude blog posts with the "dev-update" tag
limit: quantity,
skip: (page - 1) * quantity,
}),
);
2021-09-03 15:21:12 +02:00
const results = await this.generateEntries(_entries, 'post');
return {
entries: results.entries as Array<IPost>,
total: results.total,
};
}
2021-02-08 00:25:27 +01:00
public async fetchPageEntries(): Promise<TPages> {
try {
2021-09-03 15:21:12 +02:00
const _entries = await this.client.getEntries(
loadOptions({
content_type: 'splitPage', // only fetch blog post entry
order: 'fields.order',
}),
);
2021-02-08 00:25:27 +01:00
2021-09-03 15:21:12 +02:00
const results = await this.generateEntries(_entries, 'splitPage');
const pages: TPages = {};
2021-02-08 00:25:27 +01:00
2021-09-03 15:21:12 +02:00
results.entries.forEach(page => {
const pageExists = SideMenuItem[page.id];
2021-02-11 01:38:40 +01:00
2021-09-03 15:21:12 +02:00
if (pageExists) {
pages[page.id] = page;
}
});
2021-02-08 00:25:27 +01:00
2021-09-03 15:21:12 +02:00
return pages;
2021-02-08 00:25:27 +01:00
} catch (e) {
return {};
}
}
2021-09-03 15:21:12 +02:00
public async fetchFAQItems(): Promise<IFetchFAQItemsReturn> {
const _entries = await this.client.getEntries({
content_type: 'faq_item', // only fetch faq items
order: 'fields.id',
});
const results = await this.generateEntries(_entries, 'faq');
return {
entries: results.entries as Array<IFAQItem>,
total: results.total,
};
}
public async fetchEntryById(id): Promise<IPost> {
return this.client.getEntry(id).then(entry => {
if (entry) {
return this.convertPost(entry);
}
return null;
});
}
public async fetchEntryBySlug(
slug: string,
entryType: 'post' | 'splitPage',
): Promise<any> {
const _entries = await this.client.getEntries({
content_type: entryType, // only fetch specific type
'fields.slug': slug,
});
if (_entries?.items?.length > 0) {
let entry;
switch (entryType) {
case 'post':
entry = this.convertPost(_entries.items[0]);
break;
case 'splitPage':
entry = this.convertPage(_entries.items[0]);
break;
default:
break;
}
return entry;
}
return Promise.reject(
new Error(`Failed to fetch ${entryType} for ${slug}`),
);
}
2021-02-11 01:38:40 +01:00
public async fetchPageById(id: SideMenuItem): Promise<ISplitPage> {
2021-02-02 06:28:00 +01:00
return this.client
.getEntries(
loadOptions({
content_type: 'splitPage',
'fields.id[in]': id,
}),
)
2021-02-02 06:28:00 +01:00
.then(entries => {
if (entries && entries.items && entries.items.length > 0) {
2021-02-08 00:25:27 +01:00
return this.convertPage(entries.items[0]);
2021-02-02 06:28:00 +01:00
}
return null;
});
}
2021-09-03 15:21:12 +02:00
public async generateEntries(
entries: EntryCollection<unknown>,
entryType: 'post' | 'faq' | 'splitPage',
): Promise<IFetchEntriesReturn> {
let _entries: any = [];
2021-05-10 07:36:02 +02:00
if (entries && entries.items && entries.items.length > 0) {
2021-09-03 15:21:12 +02:00
switch (entryType) {
case 'post':
_entries = entries.items.map(entry => this.convertPost(entry));
break;
case 'faq':
_entries = entries.items.map(entry => this.convertFAQ(entry));
break;
case 'splitPage':
_entries = entries.items.map(entry => this.convertPage(entry));
break;
default:
break;
}
return { entries: _entries, total: entries.total };
2021-05-10 07:36:02 +02:00
}
2021-09-03 15:21:12 +02:00
return { entries: _entries, total: 0 };
2021-05-10 07:36:02 +02:00
}
2021-01-31 11:02:19 +01:00
public convertImage = (rawImage): IFigureImage =>
rawImage
? {
2021-02-24 02:02:47 +01:00
imageUrl: rawImage.file.url.replace('//', 'https://'), // may need to put null check as well here
2021-02-02 06:28:00 +01:00
description: rawImage.description ?? null,
title: rawImage.title ?? null,
2021-09-03 15:21:12 +02:00
width: rawImage.file.details.image.width,
height: rawImage.file.details.image.height,
2021-01-31 11:02:19 +01:00
}
: null;
2021-01-29 03:50:49 +01:00
2021-01-31 11:02:19 +01:00
public convertAuthor = (rawAuthor): IAuthor =>
rawAuthor
? {
2021-02-11 06:26:56 +01:00
name: rawAuthor?.name ?? null,
2021-02-02 06:28:00 +01:00
avatar: this.convertImage(rawAuthor.avatar.fields),
2021-02-11 06:26:56 +01:00
shortBio: rawAuthor?.shortBio ?? null,
position: rawAuthor?.position ?? null,
email: rawAuthor?.email ?? null,
2021-01-31 11:02:19 +01:00
twitter: rawAuthor?.twitter ?? null,
2021-02-01 03:57:54 +01:00
facebook: rawAuthor.facebook ?? null,
github: rawAuthor.github ?? null,
2021-01-31 11:02:19 +01:00
}
: null;
2021-01-29 03:50:49 +01:00
2021-01-31 11:02:19 +01:00
public convertPost = (rawData): IPost => {
2021-01-29 03:50:49 +01:00
const rawPost = rawData.fields;
2021-02-02 06:28:00 +01:00
const rawFeatureImage = rawPost?.featureImage
? rawPost?.featureImage.fields
2021-01-29 03:50:49 +01:00
: null;
const rawAuthor = rawPost.author ? rawPost.author.fields : null;
2021-01-31 11:02:19 +01:00
2021-01-29 03:50:49 +01:00
return {
2021-02-08 07:37:50 +01:00
id: rawData.sys.id ?? null,
body: rawPost.body ?? null,
subtitle: rawPost.subtitle ?? null,
description: rawPost.description ?? null,
2021-09-19 04:27:09 +02:00
publishedDateISO: rawPost.date,
2021-09-02 15:59:59 +02:00
publishedDate: format(parseISO(rawPost.date), 'dd MMMM yyyy'),
2021-01-29 03:50:49 +01:00
slug: rawPost.slug,
2021-02-18 04:07:53 +01:00
tags: rawPost?.tags, //?.map(t => t?.fields?.label) ?? [],
2021-01-29 03:50:49 +01:00
title: rawPost.title,
featureImage: this.convertImage(rawFeatureImage),
author: this.convertAuthor(rawAuthor),
};
};
2021-02-04 07:19:04 +01:00
public convertPage = (rawData): ISplitPage => {
const rawPage = rawData.fields;
const rawHero = rawPage?.hero ? rawPage?.hero?.fields : null;
return {
2021-02-11 01:38:40 +01:00
id: SideMenuItem[rawPage?.id] ?? null,
2021-02-10 09:38:27 +01:00
label: rawPage?.label ?? null,
title: rawPage?.title ?? null,
body: rawPage?.body ?? null,
2021-02-04 07:19:04 +01:00
hero: this.convertImage(rawHero),
};
};
2021-05-10 07:36:02 +02:00
public convertFAQ = (rawData): IFAQItem => {
const rawFAQ = rawData.fields;
const { question, answer, id } = rawFAQ;
return {
id: id ?? null,
question: question ?? null,
answer: answer ?? null,
};
};
2021-01-29 03:50:49 +01:00
}
2021-02-12 06:37:44 +01:00
2021-02-15 04:22:26 +01:00
const extractShortcodeGeneralButton = (shortcode: string) => {
if (!CMS.SHORTCODES.GENERAL_BUTTON.test(shortcode)) {
2021-02-12 06:37:44 +01:00
return null;
}
// Pull our href and text
2021-02-15 04:22:26 +01:00
const href = shortcode
.replace(/^{{[\s]*button[\s]*href="/, '')
.replace(/"[\s]*text="[^"]{1,99}"[\s]*}}/, '');
const text = shortcode
2021-02-23 05:36:13 +01:00
.replace(/^{{[\s]*button[\s]*href="[^"]{1,333}"[\s]*text="/, '')
2021-02-15 04:22:26 +01:00
.replace(/"[\s]*}}$/, '');
return { href, text };
};
export const renderShortcode = (shortcode: string) => {
// General button
if (CMS.SHORTCODES.GENERAL_BUTTON.test(shortcode)) {
const { href, text } = extractShortcodeGeneralButton(shortcode);
return (
2021-02-23 05:36:13 +01:00
<div className="flex justify-center mt-2 mb-4">
2021-02-15 04:22:26 +01:00
<Button onClick={() => open(href, '_blank')}>{text}</Button>
</div>
);
}
2021-02-18 04:07:53 +01:00
// Community links
if (CMS.SHORTCODES.COMMUNITY_LINKS.test(shortcode)) {
// Community links - Telegram, Discord, Reddit, etc
return (
<div className="flex justify-center mt-6 mb-4 space-x-4">
<RedditSVG
className="h-8 cursor-pointer"
onClick={() => open('https://www.reddit.com/r/oxen_io', '_blank')}
/>
<TelegramSVG
className="h-8 cursor-pointer"
onClick={() => open('https://t.me/Oxen_Community', '_blank')}
/>
<DiscordSVG
className="h-8 cursor-pointer"
onClick={() => open('https://discord.com/invite/67GXfD6', '_blank')}
/>
</div>
);
}
2021-02-25 05:33:47 +01:00
// Trade links on "Why buy $OXEN?"
if (CMS.SHORTCODES.TRADE_LINKS.test(shortcode)) {
return (
2021-02-25 06:16:55 +01:00
<div className="flex flex-col items-center mt-6 space-y-4">
2021-02-25 05:38:13 +01:00
<h4 className="text-lg font-bold tracking-wide">Find $OXEN on</h4>
2021-02-25 05:33:47 +01:00
<div className="flex justify-center mb-4 space-x-4">
<Button
wide
prefix={<KucoinSVG className="h-4" />}
onClick={() => open('https://trade.kucoin.com/LOKI-USDT', '_blank')}
type="ghost"
>
Kucoin
</Button>
<Button
wide
prefix={<BittrexSVG className="h-4" />}
onClick={() =>
open(
'https://global.bittrex.com/Market/Index?MarketName=USDT-OXEN',
'_blank',
)
}
type="ghost"
>
Bittrex
</Button>
</div>
2021-02-25 05:38:13 +01:00
</div>
2021-02-25 05:33:47 +01:00
);
}
2021-02-15 04:22:26 +01:00
// Github links
if (CMS.SHORTCODES.GITHUB_LINKS.test(shortcode)) {
// oxen core, session android/desktop/ios, lokinet
return (
<>
2021-02-18 04:07:53 +01:00
<div className="flex flex-wrap justify-center mt-6 mb-4 space-x-4">
<Button
onClick={() =>
open('https://github.com/oxen-io/oxen-core', '_blank')
}
type="ghost"
>
Oxen Core
</Button>
<Button
onClick={() =>
open('https://github.com/oxen-io/loki-network', '_blank')
}
type="ghost"
>
Lokinet
</Button>
2021-02-15 04:22:26 +01:00
</div>
2021-02-18 04:07:53 +01:00
2021-02-15 04:22:26 +01:00
<div className="flex flex-wrap justify-center mb-4 space-x-4">
2021-02-18 04:07:53 +01:00
<Button
className="mb-4"
onClick={() =>
open('https://github.com/oxen-io/session-android', '_blank')
}
type="ghost"
>
Session Android
</Button>
<Button
className="mb-4"
onClick={() =>
open('https://github.com/oxen-io/session-ios', '_blank')
}
type="ghost"
>
Session iOS
</Button>
<Button
className="mb-4"
onClick={() =>
open('https://github.com/oxen-io/session-desktop', '_blank')
}
type="ghost"
>
Session Desktop
</Button>
2021-02-15 04:22:26 +01:00
</div>
</>
);
}
2021-02-23 05:36:09 +01:00
// Call to Action -> Who Uses Oxen
if (CMS.SHORTCODES.CTA_WHO_USES_OXEN.test(shortcode)) {
return (
<div className="flex justify-center mt-6 mb-4 space-x-4">
<Button
onClick={() => open('https://getsession.org', '_blank')}
type="ghost"
>
Get Session
</Button>
<Button
onClick={() =>
open(
'https://docs.oxen.io/using-the-oxen-blockchain/overview',
'_blank',
)
}
type="ghost"
>
Use Oxen
</Button>
</div>
);
}
// Call to Action -> Session & Lokinet
if (CMS.SHORTCODES.CTA_SESSION_LOKINET.test(shortcode)) {
return (
<div className="flex justify-center mt-6 mb-4 space-x-4">
<Button
onClick={() => open('https://getsession.org', '_blank')}
type="ghost"
>
Get Session
</Button>
<Button
onClick={() => open('https://lokinet.org', '_blank')}
type="ghost"
>
Use Lokinet
</Button>
</div>
);
}
// Call to Action -> Email Signup
if (CMS.SHORTCODES.CTA_EMAIL_SIGNUP.test(shortcode)) {
return <EmailSignup />;
}
2021-02-15 04:22:26 +01:00
// All shortcode buttons with simple hrefs
const shortcodeButton = Object.values(CMS.SHORTCODE_BUTTONS).find(item =>
item.regex.test(shortcode),
);
if (shortcodeButton) {
return (
<div className="flex justify-center mb-4">
<Button onClick={() => open(shortcodeButton.href, '_blank')}>
{shortcodeButton.text}
</Button>
</div>
);
}
return null;
2021-02-12 06:37:44 +01:00
};
async function loadMetaData(node: Block | Inline) {
// is embedded link not embedded media
if (!node.data.target.fields.file) {
if (node.data.target.sys.contentType.sys.id === 'post') {
node.data.target.fields.url = generateURL(
`/blog/${node.data.target.fields.slug}`,
);
}
node.data.target.fields.meta = await fetchContent(
node.data.target.fields.url,
);
}
return node;
}
export async function generateLinkMeta(doc: Document): Promise<Document> {
const promises = doc.content.map(async (node: Block | Inline) => {
if (node.nodeType === 'embedded-entry-block') {
node = await loadMetaData(node);
} else {
// check for inline embedding
const innerPromises = node.content.map(async innerNode => {
if (
innerNode.nodeType === 'embedded-entry-inline' &&
innerNode.data.target.sys.contentType.sys.id !== 'markup'
) {
innerNode = await loadMetaData(innerNode);
}
});
await Promise.all(innerPromises);
}
});
await Promise.all(promises);
return doc;
}