mirror of
https://github.com/oxen-io/oxen-website.git
synced 2023-12-13 21:00:18 +01:00
Mid-afternoon-save
This commit is contained in:
parent
215fdd2e37
commit
3251aefdb1
|
@ -3,20 +3,25 @@ import React from 'react';
|
|||
|
||||
interface Props {
|
||||
tag: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
export function TagBlock(props: Props) {
|
||||
const { tag } = props;
|
||||
const { tag, size = 'small' } = props;
|
||||
const href = `/blog?tag=${tag.toLowerCase()}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
style={{ width: 'min-content' }}
|
||||
className={classNames(
|
||||
'px-2 text-xs cursor-pointer rounded-full border border-secondary bg-secondary bg-opacity-25 text-primary font-thin',
|
||||
'flex items-center cursor-pointer rounded-full border border-secondary bg-secondary bg-opacity-25 text-primary font-thin',
|
||||
size === 'small' && 'h-4 text-xs',
|
||||
size === 'medium' && 'h-5 text-sm',
|
||||
size === 'medium' && 'h-6 text-base',
|
||||
)}
|
||||
>
|
||||
<p className="flex items-center h-4">{tag.toLowerCase()}</p>
|
||||
<span className="px-2">{tag.toLowerCase()}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,9 @@ export function TagRow({ tags }: Props) {
|
|||
// Maximum of three tags
|
||||
.slice(0, 3)
|
||||
.map(tag => (
|
||||
<TagBlock key={tag} tag={tag} />
|
||||
<div key={tag} className="mb-2">
|
||||
<TagBlock tag={tag} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ export function ArticleSectionFeatureImage({ featureImage }: Props) {
|
|||
>
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={featureImage.imageUrl}
|
||||
src={featureImage?.imageUrl}
|
||||
alt={featureImage.description ?? ''}
|
||||
style={{ objectFit: 'cover' }}
|
||||
className="w-full h-full"
|
||||
|
|
|
@ -10,10 +10,10 @@ interface Props {
|
|||
export function ArticleWidgetAuthor({ author, publishedDate }: Props) {
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar size={10} imageSrc={author?.avatar.imageUrl} />
|
||||
<Avatar size={10} imageSrc={author?.avatar?.imageUrl} />
|
||||
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="text-sm font-bold tracking-wider font-sans">
|
||||
<span className="font-sans text-sm font-bold tracking-wider">
|
||||
By: {author?.name}
|
||||
</span>
|
||||
<span>{publishedDate}</span>
|
||||
|
|
|
@ -37,7 +37,7 @@ export function ArticleCard(props: IPost): JSX.Element {
|
|||
onClick={() => router.push(href, as)}
|
||||
className="relative aspect-w-16 aspect-h-9"
|
||||
>
|
||||
{featureImage.imageUrl && (
|
||||
{featureImage?.imageUrl && (
|
||||
<img
|
||||
className="object-cover cursor-pointer"
|
||||
src={featureImage?.imageUrl}
|
||||
|
@ -76,7 +76,7 @@ export function ArticleCard(props: IPost): JSX.Element {
|
|||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="mt-2 font-sans text-xs text-gray-800">
|
||||
{publishedDate} — {author.name}
|
||||
{publishedDate} — {author?.name}
|
||||
</p>
|
||||
<TagRow tags={tags} />
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ export function ArticleCardFeature(props: IPost) {
|
|||
style={{ minHeight: '100%' }}
|
||||
className="bg-opacity-25 cursor-pointer bg-primary aspect-w-14 aspect-h-8"
|
||||
>
|
||||
<img className="object-cover" src={featureImage.imageUrl} />
|
||||
<img className="object-cover" src={featureImage?.imageUrl} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -61,14 +61,14 @@ export function ArticleCardFeature(props: IPost) {
|
|||
style={{ maxHeight: '7.25em' }}
|
||||
className="overflow-hidden text-sm leading-tight"
|
||||
>
|
||||
{description.substring(0, 250)}...
|
||||
{description?.substring(0, 250)}...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<p className="mt-3 mb-2 font-sans text-xs font-thin">
|
||||
{publishedDate} — {author.name}
|
||||
{publishedDate} — {author?.name}
|
||||
</p>
|
||||
|
||||
<TagRow tags={tags} />
|
||||
|
|
|
@ -31,8 +31,8 @@ export function ArticleCardRow(post: IPost) {
|
|||
>
|
||||
{post?.featureImage?.imageUrl && (
|
||||
<img
|
||||
src={post.featureImage.imageUrl}
|
||||
alt={post.featureImage.description}
|
||||
src={post.featureImage?.imageUrl}
|
||||
alt={post.featureImage?.description}
|
||||
className="object-cover w-full h-full rounded-lg"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -12,8 +12,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function Layout({ children }: Props) {
|
||||
const { isTablet, isDesktop } = useContext(ScreenContext);
|
||||
const { pageType, sideMenuExpanded } = useSelector(
|
||||
const { isMobile, isTablet, isDesktop } = useContext(ScreenContext);
|
||||
const { pageType, headerMobileMenuExpanded } = useSelector(
|
||||
(state: IState) => state.navigation,
|
||||
);
|
||||
|
||||
|
@ -22,6 +22,10 @@ export default function Layout({ children }: Props) {
|
|||
? UI.SIDE_MENU_SIDE_BAR_WIDTH_PX
|
||||
: 0;
|
||||
|
||||
const mobileMenuOpen =
|
||||
(pageType === PageType.BLOG || pageType === PageType.POST) &&
|
||||
headerMobileMenuExpanded;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: '100vh', width: '100%' }}
|
||||
|
@ -39,6 +43,7 @@ export default function Layout({ children }: Props) {
|
|||
<div
|
||||
style={{
|
||||
marginLeft: `${marginLeft}px`,
|
||||
filter: `brightness(${mobileMenuOpen ? 0.85 : 1})`,
|
||||
}}
|
||||
className="relative w-full h-full overflow-y-auto duration-300 bg-alt"
|
||||
>
|
||||
|
|
|
@ -38,34 +38,36 @@ export function DesktopHeader() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center ml-6 space-x-4 text-sm">
|
||||
{NAVIGATION.MENU_ITEMS.map(item => {
|
||||
const link = (
|
||||
<a
|
||||
className={classNames(
|
||||
'uppercase whitespace-no-wrap cursor-pointer',
|
||||
item.subtle
|
||||
? 'text-xs hover:underline'
|
||||
: 'duration-300 text-base font-bold py-1 px-2 hover:bg-primary rounded hover:bg-opacity-10',
|
||||
)}
|
||||
target={item.newTab ? '_blank' : undefined}
|
||||
rel={item.newTab ? 'noreferrer' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
);
|
||||
{NAVIGATION.MENU_ITEMS.filter(item => !item.mobileMenuOnly).map(
|
||||
item => {
|
||||
const link = (
|
||||
<a
|
||||
className={classNames(
|
||||
'uppercase whitespace-no-wrap cursor-pointer',
|
||||
item.subtle
|
||||
? 'text-xs hover:underline'
|
||||
: 'duration-300 text-base font-bold py-1 px-2 hover:bg-primary rounded hover:bg-opacity-10',
|
||||
)}
|
||||
target={item.newTab ? '_blank' : undefined}
|
||||
rel={item.newTab ? 'noreferrer' : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={uuid()}>
|
||||
{item.external ? (
|
||||
link
|
||||
) : (
|
||||
<Link href={item.href} as={item.href}>
|
||||
{link}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div key={uuid()}>
|
||||
{item.external ? (
|
||||
link
|
||||
) : (
|
||||
<Link href={item.href} as={item.href}>
|
||||
{link}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useClickAway } from 'react-use';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NAVIGATION, UI } from '../../constants';
|
||||
import { collapseMobileHeader } from '../../state/navigation';
|
||||
import { IState } from '../../state/reducers';
|
||||
|
||||
export function MobileMenu() {
|
||||
|
@ -10,8 +12,15 @@ export function MobileMenu() {
|
|||
(state: IState) => state.navigation,
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const ref = useRef(null);
|
||||
useClickAway(ref, () => {
|
||||
dispatch(collapseMobileHeader());
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
top: `${UI.HEADER_HEIGHT_PX}px`,
|
||||
left: `${UI.SIDE_MENU_SIDE_BAR_WIDTH_PX}px`,
|
||||
|
@ -23,18 +32,18 @@ export function MobileMenu() {
|
|||
style={{
|
||||
transform: expanded ? 'translateY(0)' : 'translateY(-100%)',
|
||||
}}
|
||||
className="flex flex-col duration-300 bg-gray-100"
|
||||
className="flex flex-col duration-300 bg-alt"
|
||||
>
|
||||
{NAVIGATION.MENU_ITEMS.map(item => {
|
||||
return (
|
||||
<div key={uuid()} className="flex justify-center px-6 py-1">
|
||||
<div key={uuid()} className="flex justify-center">
|
||||
{item.external ? (
|
||||
<a className="w-full py-4 text-lg text-center uppercase focus:bg-secondary hover:bg-secondary focus:text-white hover:text-white">
|
||||
<a className="w-full py-4 text-lg text-center uppercase cursor-pointer focus:bg-secondary hover:bg-secondary focus:text-white hover:text-white">
|
||||
{item.label}
|
||||
</a>
|
||||
) : (
|
||||
<Link key={item.label} href={item.href} as={item.href}>
|
||||
<a className="w-full py-4 text-lg text-center uppercase focus:bg-secondary hover:bg-secondary focus:text-white hover:text-white">
|
||||
<a className="w-full py-4 text-lg text-center uppercase cursor-pointer focus:bg-secondary hover:bg-secondary focus:text-white hover:text-white">
|
||||
{item.label}
|
||||
</a>
|
||||
</Link>
|
||||
|
|
|
@ -88,7 +88,7 @@ export function SideMenuSideBar({ mode }: Props) {
|
|||
'flex items-center justify-start w-0 h-0 duration-300 transform -rotate-90',
|
||||
)}
|
||||
>
|
||||
<span className="whitespace-no-wrap">
|
||||
<span className="whitespace-nowrap">
|
||||
{mode === SideBarMode.LABEL ? label : 'Menu'}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface IMenuItem {
|
|||
newTab: boolean;
|
||||
subtle: boolean;
|
||||
external: boolean;
|
||||
mobileMenuOnly?: boolean;
|
||||
}
|
||||
|
||||
// Hrefs are generated from Keys using slugify.
|
||||
|
@ -70,6 +71,7 @@ const MENU_ITEMS: IMenuItem[] = [
|
|||
newTab: false,
|
||||
subtle: false,
|
||||
external: false,
|
||||
mobileMenuOnly: true,
|
||||
},
|
||||
{
|
||||
label: 'Explorer',
|
||||
|
|
|
@ -8,18 +8,39 @@ import { PageType, setPageType, setPostTitle } from '../../state/navigation';
|
|||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle } from '../../utils/metadata';
|
||||
|
||||
export async function getServerSideProps({ params }) {
|
||||
interface IPath {
|
||||
params: { slug: string };
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
// Get paths to all pages
|
||||
// Hardcoded in navigation constants.
|
||||
// Contentful can edit entries but cannot add/remove
|
||||
// without touching code.
|
||||
const api = new CmsApi();
|
||||
const posts = await api.fetchBlogEntries();
|
||||
|
||||
const paths: IPath[] = posts.map(item => ({
|
||||
params: { slug: `/blog/${item.slug}` },
|
||||
}));
|
||||
|
||||
return { paths, fallback: true };
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
const api = new CmsApi();
|
||||
const post = await api.fetchBlogBySlug(String(params?.slug) ?? '');
|
||||
|
||||
if (!post) {
|
||||
return {
|
||||
props: undefined,
|
||||
notFound: true,
|
||||
};
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return { props: { post } };
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
},
|
||||
revalidate: 60,
|
||||
};
|
||||
}
|
||||
|
||||
function Post({ post }: { post: IPost }) {
|
||||
|
@ -33,7 +54,7 @@ function Post({ post }: { post: IPost }) {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{generateTitle(post.title)}</title>
|
||||
<title>{generateTitle(post?.title)}</title>
|
||||
</Head>
|
||||
|
||||
<Article {...post} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -6,50 +6,92 @@ import { ArticleCard } from '../../components/cards/ArticleCard';
|
|||
import { ArticleCardFeature } from '../../components/cards/ArticleCardFeature';
|
||||
import { CardGrid } from '../../components/cards/CardGrid';
|
||||
import { Contained } from '../../components/Contained';
|
||||
import { TagBlock } from '../../components/TagBlock';
|
||||
import { CmsApi } from '../../services/cms';
|
||||
import { PageType, setPageType } from '../../state/navigation';
|
||||
import { IPost } from '../../types/cms';
|
||||
import { generateTitle } from '../../utils/metadata';
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async context => {
|
||||
const api = new CmsApi();
|
||||
|
||||
// Get tag query
|
||||
const tag = String(context.query.tag ?? '') ?? null;
|
||||
|
||||
// Fetch posts even when tag, for related etc
|
||||
const posts = await api.fetchBlogEntries();
|
||||
|
||||
// const { url } = context.req;
|
||||
// const onBlog = NAVIGATION.BLOG_REGEX.test(url);
|
||||
// const onPost = NAVIGATION.POST_REGEX.test(url);
|
||||
// Todo, instead of making 2 reqs, filter over 1 req
|
||||
// const tagPosts = tag ? await api.fetchBlogEntriesByTag(tag ?? '') : [];
|
||||
const tagPosts = posts.filter(post => post.tags.includes(tag));
|
||||
|
||||
// const pageType = onBlog
|
||||
// ? PageType.BLOG
|
||||
// : onPost
|
||||
// ? PageType.POST
|
||||
// : PageType.NORMAL;
|
||||
|
||||
return { props: { posts } };
|
||||
return { props: { posts, tagPosts, tag } };
|
||||
};
|
||||
|
||||
const Blog = ({
|
||||
posts,
|
||||
pageType,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||
interface Props {
|
||||
posts: IPost[];
|
||||
tagPosts: IPost[];
|
||||
tag: string | null;
|
||||
}
|
||||
|
||||
const Blog = ({ posts, tagPosts, tag }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPageType(PageType.BLOG));
|
||||
}, []);
|
||||
|
||||
const tagHasPosts = tagPosts && tagPosts?.length > 0;
|
||||
const [featuredPost, ...otherPosts] = posts;
|
||||
|
||||
console.log('index ➡️ tag:', tag);
|
||||
console.log('index ➡️ tagHasPosts:', tagHasPosts);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{generateTitle('Blog')}</title>
|
||||
</Head>
|
||||
|
||||
<div className="flex flex-col w-full mt-6 space-y-10">
|
||||
<div className="flex flex-col w-full mt-6 mb-6 space-y-10">
|
||||
<Contained>
|
||||
<ArticleCardFeature {...posts[0]} />
|
||||
{!tag && posts.length && <ArticleCardFeature {...featuredPost} />}
|
||||
|
||||
{tag && (
|
||||
<>
|
||||
<div className="flex items-center w-full mt-4 space-x-2 font-sans">
|
||||
<p className={tagHasPosts ? 'mb-0' : 'mb-10'}>
|
||||
{tagHasPosts
|
||||
? 'Tag Results:'
|
||||
: 'There are no posts with the tag'}
|
||||
</p>
|
||||
<TagBlock size="large" tag={tag} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Contained>
|
||||
|
||||
{/* Tag has posts */}
|
||||
{tag && tagHasPosts && (
|
||||
<>
|
||||
<CardGrid>
|
||||
{(tag ? posts : otherPosts)?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Contained>
|
||||
{tag && (
|
||||
<h3 className="-mb-6 text-2xl font-prompt text-primary">
|
||||
Recent Posts
|
||||
</h3>
|
||||
)}
|
||||
</Contained>
|
||||
|
||||
<CardGrid>
|
||||
{[...posts, ...posts, ...posts, ...posts, ...posts]?.map(post => (
|
||||
{(tag ? posts : otherPosts)?.map(post => (
|
||||
<ArticleCard key={post.id} {...post} />
|
||||
))}
|
||||
</CardGrid>
|
||||
|
|
|
@ -26,7 +26,10 @@ export class CmsApi {
|
|||
})
|
||||
.then(entries => {
|
||||
if (entries && entries.items && entries.items.length > 0) {
|
||||
console.log('cms ➡️ entries:', entries);
|
||||
|
||||
const blogPosts = entries.items.map(entry => this.convertPost(entry));
|
||||
|
||||
return blogPosts;
|
||||
}
|
||||
return [];
|
||||
|
@ -57,6 +60,21 @@ export class CmsApi {
|
|||
});
|
||||
}
|
||||
|
||||
public async fetchBlogEntriesByTag(tag: string): Promise<IPost[]> {
|
||||
return this.client
|
||||
.getEntries({
|
||||
content_type: 'post',
|
||||
'fields.tags.sys.id[in]': tag,
|
||||
})
|
||||
.then(entries => {
|
||||
if (entries && entries.items && entries.items.length > 0) {
|
||||
const posts = entries.items.map(entry => this.convertPost(entry));
|
||||
return posts;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
public async fetchPageEntries(): Promise<TPages> {
|
||||
try {
|
||||
const entries = await this.client.getEntries({
|
||||
|
@ -109,11 +127,11 @@ export class CmsApi {
|
|||
public convertAuthor = (rawAuthor): IAuthor =>
|
||||
rawAuthor
|
||||
? {
|
||||
name: rawAuthor.name,
|
||||
name: rawAuthor?.name ?? null,
|
||||
avatar: this.convertImage(rawAuthor.avatar.fields),
|
||||
shortBio: rawAuthor.shortBio,
|
||||
position: rawAuthor.position,
|
||||
email: rawAuthor.email,
|
||||
shortBio: rawAuthor?.shortBio ?? null,
|
||||
position: rawAuthor?.position ?? null,
|
||||
email: rawAuthor?.email ?? null,
|
||||
twitter: rawAuthor?.twitter ?? null,
|
||||
facebook: rawAuthor.facebook ?? null,
|
||||
github: rawAuthor.github ?? null,
|
||||
|
@ -132,7 +150,7 @@ export class CmsApi {
|
|||
body: rawPost.body ?? null,
|
||||
subtitle: rawPost.subtitle ?? null,
|
||||
description: rawPost.description ?? null,
|
||||
publishedDate: moment(rawPost.publishedDate).format('DD MMMM YYYY'),
|
||||
publishedDate: moment(rawPost.date).format('DD MMMM YYYY'),
|
||||
slug: rawPost.slug,
|
||||
tags: rawPost?.tags?.map(t => t?.fields?.label) ?? [],
|
||||
title: rawPost.title,
|
||||
|
|
Loading…
Reference in a new issue