Mid-afternoon-save

This commit is contained in:
Vince 2021-02-11 16:26:56 +11:00
parent 215fdd2e37
commit 3251aefdb1
15 changed files with 186 additions and 80 deletions

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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} />

View file

@ -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"
/>
)}

View file

@ -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"
>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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',

View file

@ -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} />

View file

@ -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>

View file

@ -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,