Added blog components
This commit is contained in:
parent
e959e6fc3f
commit
43d161b3d1
15 changed files with 394 additions and 0 deletions
40
blog/alert.tsx
Normal file
40
blog/alert.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import Container from "@/components/blog/container";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
preview?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Alert = ({ preview }: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("border-b dark:bg-slate-800", {
|
||||||
|
"bg-neutral-800 border-neutral-800 text-white": preview,
|
||||||
|
"bg-neutral-50 border-neutral-200": !preview,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<div className="py-2 text-center text-sm">
|
||||||
|
{preview ? (
|
||||||
|
<>
|
||||||
|
This page is a preview.{" "}
|
||||||
|
<a
|
||||||
|
href="/api/exit-preview"
|
||||||
|
className="underline hover:text-teal-300 duration-200 transition-colors"
|
||||||
|
>
|
||||||
|
Click here
|
||||||
|
</a>{" "}
|
||||||
|
to exit preview mode.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Thunder Network
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Alert;
|
15
blog/avatar.tsx
Normal file
15
blog/avatar.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
type Props = {
|
||||||
|
name: string;
|
||||||
|
picture: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Avatar = ({ name, picture }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={picture} className="w-12 h-12 rounded-full mr-4" alt={name} />
|
||||||
|
<div className="text-xl font-bold">{name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Avatar;
|
9
blog/container.tsx
Normal file
9
blog/container.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
type Props = {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Container = ({ children }: Props) => {
|
||||||
|
return <div className="container mx-auto px-5">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Container;
|
36
blog/cover-image.tsx
Normal file
36
blog/cover-image.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {cn} from "@/lib/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
src: string;
|
||||||
|
slug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CoverImage = ({ title, src, slug }: Props) => {
|
||||||
|
const image = (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={`Cover Image for ${title}`}
|
||||||
|
className={cn("shadow-sm w-full", {
|
||||||
|
"hover:shadow-lg transition-shadow duration-200": slug,
|
||||||
|
})}
|
||||||
|
width={1300}
|
||||||
|
height={630}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="sm:mx-0">
|
||||||
|
{slug ? (
|
||||||
|
<Link href={`/blog/${slug}`} aria-label={title}>
|
||||||
|
{image}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
image
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CoverImage;
|
12
blog/date-formatter.tsx
Normal file
12
blog/date-formatter.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { parseISO, format } from "date-fns";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dateString: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateFormatter = ({ dateString }: Props) => {
|
||||||
|
const date = parseISO(dateString);
|
||||||
|
return <time dateTime={dateString}>{format(date, "LLLL d, yyyy")}</time>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DateFormatter;
|
32
blog/footer.tsx
Normal file
32
blog/footer.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import Container from "@/components/blog/container";
|
||||||
|
import { EXAMPLE_PATH } from "@/lib/blog/constants";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-neutral-50 border-t border-neutral-200 dark:bg-slate-800">
|
||||||
|
<Container>
|
||||||
|
<div className="py-28 flex flex-col lg:flex-row items-center">
|
||||||
|
<h3 className="text-4xl lg:text-[2.5rem] font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2">
|
||||||
|
Statically Generated with Next.js.
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2">
|
||||||
|
<a
|
||||||
|
href="https://nextjs.org/docs/app/building-your-application/routing/layouts-and-templates"
|
||||||
|
className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0"
|
||||||
|
>
|
||||||
|
Read Documentation
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`}
|
||||||
|
className="mx-3 font-bold hover:underline"
|
||||||
|
>
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
14
blog/header.tsx
Normal file
14
blog/header.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
return (
|
||||||
|
<h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8 flex items-center">
|
||||||
|
<Link href="/" className="hover:underline">
|
||||||
|
Blog
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
47
blog/hero-post.tsx
Normal file
47
blog/hero-post.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import Avatar from "@/components/blog/avatar";
|
||||||
|
import CoverImage from "@/components/blog/cover-image";
|
||||||
|
import { type Author } from "@/interfaces/author";
|
||||||
|
import Link from "next/link";
|
||||||
|
import DateFormatter from "./date-formatter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
date: string;
|
||||||
|
excerpt: string;
|
||||||
|
author: Author;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HeroPost({
|
||||||
|
title,
|
||||||
|
coverImage,
|
||||||
|
date,
|
||||||
|
excerpt,
|
||||||
|
author,
|
||||||
|
slug,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="mb-8 md:mb-16">
|
||||||
|
<CoverImage title={title} src={coverImage} slug={slug} />
|
||||||
|
</div>
|
||||||
|
<div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28">
|
||||||
|
<div>
|
||||||
|
<h3 className="mb-4 text-4xl lg:text-5xl leading-tight">
|
||||||
|
<Link href={`/blog/${slug}`} className="hover:underline">
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<div className="mb-4 md:mb-0 text-lg">
|
||||||
|
<DateFormatter dateString={date} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
|
||||||
|
<Avatar name={author.name} picture={author.picture} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
13
blog/intro.tsx
Normal file
13
blog/intro.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
export function Intro() {
|
||||||
|
return (
|
||||||
|
<section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12">
|
||||||
|
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8">
|
||||||
|
Blog.
|
||||||
|
</h1>
|
||||||
|
<h4 className="text-center md:text-left text-lg mt-5 md:pl-8">
|
||||||
|
Thunder Network
|
||||||
|
</h4>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
29
blog/more-stories.tsx
Normal file
29
blog/more-stories.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Post } from "@/interfaces/post";
|
||||||
|
import { PostPreview } from "./post-preview";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
posts: Post[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MoreStories({ posts }: Props) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-8 text-5xl md:text-7xl font-bold tracking-tighter leading-tight">
|
||||||
|
More Stories
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<PostPreview
|
||||||
|
key={post.slug}
|
||||||
|
title={post.title}
|
||||||
|
coverImage={post.coverImage}
|
||||||
|
date={post.date}
|
||||||
|
author={post.author}
|
||||||
|
slug={post.slug}
|
||||||
|
excerpt={post.excerpt}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
34
blog/post-header.tsx
Normal file
34
blog/post-header.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import Avatar from "./avatar";
|
||||||
|
import CoverImage from "./cover-image";
|
||||||
|
import DateFormatter from "./date-formatter";
|
||||||
|
import { PostTitle } from "@/components/blog/post-title";
|
||||||
|
import { type Author } from "@/interfaces/author";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
date: string;
|
||||||
|
author: Author;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PostHeader({ title, coverImage, date, author }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PostTitle>{title}</PostTitle>
|
||||||
|
<div className="hidden md:block md:mb-12">
|
||||||
|
<Avatar name={author.name} picture={author.picture} />
|
||||||
|
</div>
|
||||||
|
<div className="mb-8 md:mb-16 sm:mx-0">
|
||||||
|
<CoverImage title={title} src={coverImage} />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="block md:hidden mb-6">
|
||||||
|
<Avatar name={author.name} picture={author.picture} />
|
||||||
|
</div>
|
||||||
|
<div className="mb-6 text-lg">
|
||||||
|
<DateFormatter dateString={date} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
41
blog/post-preview.tsx
Normal file
41
blog/post-preview.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { type Author } from "@/interfaces/author";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Avatar from "./avatar";
|
||||||
|
import CoverImage from "./cover-image";
|
||||||
|
import DateFormatter from "./date-formatter";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
coverImage: string;
|
||||||
|
date: string;
|
||||||
|
excerpt: string;
|
||||||
|
author: Author;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PostPreview({
|
||||||
|
title,
|
||||||
|
coverImage,
|
||||||
|
date,
|
||||||
|
excerpt,
|
||||||
|
author,
|
||||||
|
slug,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-5">
|
||||||
|
<CoverImage slug={slug} title={title} src={coverImage} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl mb-3 leading-snug">
|
||||||
|
<Link href={`/posts/${slug}`} className="hover:underline">
|
||||||
|
{title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<div className="text-lg mb-4">
|
||||||
|
<DateFormatter dateString={date} />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg leading-relaxed mb-4">{excerpt}</p>
|
||||||
|
<Avatar name={author.name} picture={author.picture} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
blog/post-title.tsx
Normal file
13
blog/post-title.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PostTitle({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
3
blog/section-separator.tsx
Normal file
3
blog/section-separator.tsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function SectionSeparator() {
|
||||||
|
return <hr className="border-neutral-200 mt-28 mb-24" />;
|
||||||
|
}
|
56
blog/switch.module.css
Normal file
56
blog/switch.module.css
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
.switch {
|
||||||
|
all: unset;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 70px;
|
||||||
|
display: inline-block;
|
||||||
|
color: currentColor;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px dashed currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
--size: 24px;
|
||||||
|
height: var(--size);
|
||||||
|
width: var(--size);
|
||||||
|
transition: all 0.3s ease-in-out 0s !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mode="system"] .switch::after {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: calc(var(--size) / 2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
content: "A";
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mode="light"] .switch {
|
||||||
|
box-shadow: 0 0 50px 10px yellow;
|
||||||
|
background-color: yellow;
|
||||||
|
border: 1px solid orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mode="dark"] .switch {
|
||||||
|
box-shadow: calc(var(--size) / 4) calc(var(--size) / -4) calc(var(--size) / 8)
|
||||||
|
inset #fff;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
animation: n linear 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes n {
|
||||||
|
40% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue