blog post page
This commit is contained in:
12
app/blog/components/avatar.tsx
Normal file
12
app/blog/components/avatar.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Avatar({ name, picture }: any) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 relative mr-4">
|
||||
<Image src={picture.url} layout="fill" className="rounded-full" alt={name} />
|
||||
</div>
|
||||
<div className="text-xl font-bold">{name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
28
app/blog/components/cover-image.tsx
Normal file
28
app/blog/components/cover-image.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Image } from "react-datocms";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function CoverImage({ title, responsiveImage, slug }: any) {
|
||||
const image = (
|
||||
<Image
|
||||
data={{
|
||||
...responsiveImage,
|
||||
alt: `Cover Image for ${title}`,
|
||||
}}
|
||||
className={clsx("shadow-small", {
|
||||
"hover:shadow-medium transition-shadow duration-200": slug,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className="sm:mx-0">
|
||||
{slug ? (
|
||||
<Link href={`/posts/${slug}`}>
|
||||
<a aria-label={title}>{image}</a>
|
||||
</Link>
|
||||
) : (
|
||||
image
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
10
app/blog/components/date.tsx
Normal file
10
app/blog/components/date.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
const formatter = Intl.DateTimeFormat("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
export default function DateComponent({ dateString }: any) {
|
||||
const date = new Date(dateString);
|
||||
return <time dateTime={dateString}>{formatter.format(date)}</time>;
|
||||
}
|
82
app/blog/components/more-stories.tsx
Normal file
82
app/blog/components/more-stories.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Link, Routes } from "blitz";
|
||||
import PostPreview from "./post-preview";
|
||||
import type { Post } from "../../../integrations/datocms";
|
||||
|
||||
type Props = {
|
||||
posts: Post[];
|
||||
};
|
||||
|
||||
const formatter = Intl.DateTimeFormat("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
export default function MoreStories({ posts }: Props) {
|
||||
return (
|
||||
<aside>
|
||||
<div className="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div className="pb-12 md:pb-20">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h4 className="h4 font-mackinac mb-8">Related articles</h4>
|
||||
|
||||
{/* Articles container */}
|
||||
<div className="grid gap-4 sm:gap-6 sm:grid-cols-2">
|
||||
{posts.map((post) => (
|
||||
<article key={post.slug} className="relative group p-6 text-white">
|
||||
<figure>
|
||||
<img
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-50 group-hover:opacity-75 transition duration-700 ease-out"
|
||||
src={post.coverImage.responsiveImage.src}
|
||||
width="372"
|
||||
height="182"
|
||||
alt="Related post"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-primary-500 opacity-75 group-hover:opacity-50 transition duration-700 ease-out"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</figure>
|
||||
<div className="relative flex flex-col h-full">
|
||||
<header className="flex-grow">
|
||||
<Link href={Routes.PostPage({ slug: post.slug })}>
|
||||
<a className="hover:underline">
|
||||
<h3 className="text-lg font-mackinac font-bold tracking-tight mb-2">
|
||||
{post.title}
|
||||
</h3>
|
||||
</a>
|
||||
</Link>
|
||||
<div className="text-sm opacity-80">
|
||||
{formatter.format(new Date(post.date))}
|
||||
</div>
|
||||
</header>
|
||||
<footer>
|
||||
{/* Author meta */}
|
||||
<div className="flex items-center text-sm mt-5">
|
||||
<a href="#0">
|
||||
<img
|
||||
className="rounded-full flex-shrink-0 mr-3"
|
||||
src={post.author.picture.url}
|
||||
width="32"
|
||||
height="32"
|
||||
alt={post.author.name}
|
||||
/>
|
||||
</a>
|
||||
<div>
|
||||
<span className="opacity-75">By </span>
|
||||
<span className="font-medium hover:underline">
|
||||
{post.author.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
16
app/blog/components/post-body.tsx
Normal file
16
app/blog/components/post-body.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import styles from "../styles/post-body.module.css";
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export default function PostBody({ content }: Props) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div
|
||||
className={`prose prose-lg prose-blue ${styles.markdown}`}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
25
app/blog/components/post-preview.tsx
Normal file
25
app/blog/components/post-preview.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import Avatar from "./avatar";
|
||||
import Date from "./date";
|
||||
import CoverImage from "./cover-image";
|
||||
|
||||
export default function PostPreview({ title, coverImage, date, excerpt, author, slug }: any) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-5">
|
||||
<CoverImage slug={slug} title={title} responsiveImage={coverImage.responsiveImage} />
|
||||
</div>
|
||||
<h3 className="text-3xl mb-3 leading-snug">
|
||||
<Link href={`/posts/${slug}`}>
|
||||
<a className="hover:underline">{title}</a>
|
||||
</Link>
|
||||
</h3>
|
||||
<div className="text-lg mb-4">
|
||||
<Date dateString={date} />
|
||||
</div>
|
||||
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
|
||||
<Avatar name={author.name} picture={author.picture} />
|
||||
</div>
|
||||
);
|
||||
}
|
3
app/blog/components/section-separator.tsx
Normal file
3
app/blog/components/section-separator.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function SectionSeparator() {
|
||||
return <hr className="border-accent-2 mt-28 mb-24" />;
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
import { BlitzPage, GetStaticPaths, GetStaticProps, Head, useRouter } from "blitz";
|
||||
import type { BlitzPage, GetStaticPaths, GetStaticProps } from "blitz";
|
||||
import { Head, useRouter } from "blitz";
|
||||
import ErrorPage from "next/error";
|
||||
|
||||
import type { Post } from "integrations/datocms";
|
||||
import { getAllPostsWithSlug, getPostAndMorePosts, markdownToHtml } from "integrations/datocms";
|
||||
import Header from "../../../landing-page/components/header";
|
||||
import PostBody from "../../components/post-body";
|
||||
import SectionSeparator from "../../components/section-separator";
|
||||
import MoreStories from "../../components/more-stories";
|
||||
|
||||
type Props = {
|
||||
post: Post;
|
||||
@ -10,15 +15,96 @@ type Props = {
|
||||
preview: boolean;
|
||||
};
|
||||
|
||||
const formatter = Intl.DateTimeFormat("en-US", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const PostPage: BlitzPage<Props> = ({ post, morePosts, preview }) => {
|
||||
const router = useRouter();
|
||||
if (!router.isFallback && !post?.slug) {
|
||||
return <ErrorPage statusCode={404} />;
|
||||
}
|
||||
|
||||
console.log("post", post);
|
||||
|
||||
// TODO
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen overflow-hidden">
|
||||
<Header />
|
||||
|
||||
<main className="flex-grow">
|
||||
<section className="relative">
|
||||
{/* Background image */}
|
||||
<div className="absolute inset-0 h-128 pt-16 box-content">
|
||||
<img
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-25"
|
||||
src={post.coverImage.responsiveImage.src}
|
||||
width={post.coverImage.responsiveImage.width}
|
||||
height={post.coverImage.responsiveImage.height}
|
||||
alt={post.coverImage.responsiveImage.alt ?? `${post.title} cover image`}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-t from-white dark:from-gray-900"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-6xl mx-auto px-4 sm:px-6">
|
||||
<div className="pt-32 pb-12 md:pt-40 md:pb-20">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<article>
|
||||
{/* Article header */}
|
||||
<header className="mb-8">
|
||||
{/* Title and excerpt */}
|
||||
<div className="text-center md:text-left">
|
||||
<h1 className="h1 font-mackinac mb-4">{post.title}</h1>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-400">{post.excerpt}</p>
|
||||
</div>
|
||||
{/* Article meta */}
|
||||
<div className="md:flex md:items-center md:justify-between mt-5">
|
||||
{/* Author meta */}
|
||||
<div className="flex items-center justify-center">
|
||||
<img
|
||||
className="rounded-full flex-shrink-0 mr-3"
|
||||
src={post.author.picture.url}
|
||||
width="32"
|
||||
height="32"
|
||||
alt="Author 04"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">By </span>
|
||||
<a
|
||||
className="font-medium text-gray-800 dark:text-gray-300 hover:underline"
|
||||
href="#0"
|
||||
>
|
||||
{post.author.name}
|
||||
</a>
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{" "}
|
||||
· {formatter.format(new Date(post.date))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<hr className="w-5 h-px pt-px bg-gray-400 dark:bg-gray-500 border-0 mb-8" />
|
||||
|
||||
{/* Article content */}
|
||||
<div className="text-lg text-gray-600 dark:text-gray-400">
|
||||
<PostBody content={post.content} />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<SectionSeparator />
|
||||
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
/*return (
|
||||
<Layout preview={preview}>
|
||||
<Container>
|
||||
@ -49,8 +135,6 @@ const PostPage: BlitzPage<Props> = ({ post, morePosts, preview }) => {
|
||||
</Container>
|
||||
</Layout>
|
||||
);*/
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default PostPage;
|
||||
|
18
app/blog/styles/post-body.module.css
Normal file
18
app/blog/styles/post-body.module.css
Normal file
@ -0,0 +1,18 @@
|
||||
.markdown {
|
||||
@apply text-lg leading-relaxed;
|
||||
}
|
||||
|
||||
.markdown p,
|
||||
.markdown ul,
|
||||
.markdown ol,
|
||||
.markdown blockquote {
|
||||
@apply my-6;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
@apply text-3xl mt-12 mb-4 leading-snug;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
@apply text-2xl mt-8 mb-4 leading-snug;
|
||||
}
|
Reference in New Issue
Block a user