refactor(blog) refactor blog home page
This commit is contained in:
parent
7d5f4a52a8
commit
ebeacae587
@ -26,6 +26,8 @@ export const docs = defineDocs({
|
||||
|
||||
/**
|
||||
* Changelog
|
||||
*
|
||||
* title is required, but description is optional in frontmatter
|
||||
*/
|
||||
export const changelog = defineCollections({
|
||||
type: 'doc',
|
||||
@ -39,6 +41,8 @@ export const changelog = defineCollections({
|
||||
|
||||
/**
|
||||
* Pages, like privacy policy, terms of service, etc.
|
||||
*
|
||||
* title is required, but description is optional in frontmatter
|
||||
*/
|
||||
export const pages = defineCollections({
|
||||
type: 'doc',
|
||||
@ -52,39 +56,48 @@ export const pages = defineCollections({
|
||||
/**
|
||||
* Blog authors
|
||||
*
|
||||
* description is optional, but we must add it to the schema
|
||||
* description is optional in frontmatter, but we must add it to the schema
|
||||
*/
|
||||
export const author = defineCollections({
|
||||
type: 'doc',
|
||||
dir: 'content/author',
|
||||
schema: frontmatterSchema.extend({
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
avatar: z.string(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Blog categories
|
||||
*
|
||||
* description is optional in frontmatter, but we must add it to the schema
|
||||
*/
|
||||
export const category = defineCollections({
|
||||
type: 'doc',
|
||||
dir: 'content/category',
|
||||
schema: frontmatterSchema.extend({
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Blog posts
|
||||
*
|
||||
* dtitle is required, but description is optional in frontmatter
|
||||
*/
|
||||
export const blog = defineCollections({
|
||||
type: 'doc',
|
||||
dir: 'content/blog',
|
||||
schema: frontmatterSchema.extend({
|
||||
image: z.string(),
|
||||
date: z.string().date(),
|
||||
published: z.boolean().default(true),
|
||||
categories: z.array(z.string()),
|
||||
author: z.string(),
|
||||
}),
|
||||
schema: (ctx) => {
|
||||
// console.log('ctx', ctx); // {source, path}
|
||||
return frontmatterSchema.extend({
|
||||
image: z.string(),
|
||||
date: z.string().date(),
|
||||
published: z.boolean().default(true),
|
||||
categories: z.array(z.string()),
|
||||
author: z.string(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -1,24 +1,28 @@
|
||||
import { BlogCategoryFilter } from '@/components/blog/blog-category-filter';
|
||||
import Container from '@/components/layout/container';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allCategories } from 'content-collections';
|
||||
import { categorySource } from '@/lib/docs/source';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps {}
|
||||
interface BlogListLayoutProps extends PropsWithChildren {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export default async function BlogListLayout({
|
||||
children,
|
||||
params,
|
||||
}: BlogListLayoutProps) {
|
||||
const resolvedParams = await params;
|
||||
const { locale } = resolvedParams;
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations('BlogPage');
|
||||
|
||||
// Filter categories by locale
|
||||
const categoryList = allCategories.filter(
|
||||
(category) => category.locale === locale
|
||||
);
|
||||
const language = locale as string;
|
||||
const categoryList = categorySource.getPages(language).map((category) => ({
|
||||
slug: category.slugs[0],
|
||||
name: category.data.name,
|
||||
description: category.data.description || '',
|
||||
}));
|
||||
// console.log('categoryList', categoryList);
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
|
@ -1,20 +1,25 @@
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
|
||||
import { type BlogType, authorSource, categorySource } from '@/lib/docs/source';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import type { Post } from 'content-collections';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface BlogCardProps {
|
||||
post: Post;
|
||||
locale: string;
|
||||
post: BlogType;
|
||||
}
|
||||
|
||||
export default function BlogCard({ post }: BlogCardProps) {
|
||||
const publishDate = post.date;
|
||||
const date = formatDate(new Date(publishDate));
|
||||
export default function BlogCard({ locale, post }: BlogCardProps) {
|
||||
const { date, title, description, image, author, categories } = post.data;
|
||||
const publishDate = formatDate(new Date(date));
|
||||
const blogAuthor = authorSource.getPage([author], locale);
|
||||
const blogCategories = categorySource
|
||||
.getPages(locale)
|
||||
.filter((category) => categories.includes(category.slugs[0] ?? ''));
|
||||
|
||||
// Extract the slug parts for the Link component
|
||||
const slugParts = post.slugAsParams.split('/');
|
||||
const slugParts = post.slugs[0].split('/');
|
||||
// console.log('BlogCard, slugParts', slugParts);
|
||||
|
||||
return (
|
||||
@ -22,27 +27,27 @@ export default function BlogCard({ post }: BlogCardProps) {
|
||||
<div className="group flex flex-col border rounded-lg overflow-hidden h-full">
|
||||
{/* Image container - fixed aspect ratio */}
|
||||
<div className="group overflow-hidden relative aspect-16/9 w-full">
|
||||
{post.image && (
|
||||
{image && (
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
src={post.image}
|
||||
alt={post.title || 'image for blog post'}
|
||||
title={post.title || 'image for blog post'}
|
||||
src={image}
|
||||
alt={title || 'image for blog post'}
|
||||
title={title || 'image for blog post'}
|
||||
className="object-cover hover:scale-105 transition-transform duration-300"
|
||||
placeholder="blur"
|
||||
blurDataURL={PLACEHOLDER_IMAGE}
|
||||
fill
|
||||
/>
|
||||
|
||||
{post.categories && post.categories.length > 0 && (
|
||||
{blogCategories && blogCategories.length > 0 && (
|
||||
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{post.categories.map((category, index) => (
|
||||
{blogCategories.map((category, index) => (
|
||||
<span
|
||||
key={`${category?.slug}-${index}`}
|
||||
key={`${category?.slugs[0]}-${index}`}
|
||||
className="text-xs font-medium text-white bg-black bg-opacity-50 px-2 py-1 rounded-md"
|
||||
>
|
||||
{category?.name}
|
||||
{category?.data.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@ -58,7 +63,7 @@ export default function BlogCard({ post }: BlogCardProps) {
|
||||
{/* Post title */}
|
||||
<h3 className="text-lg line-clamp-2 font-medium">
|
||||
<span
|
||||
className="bg-linear-to-r from-green-200 to-green-100
|
||||
className="bg-linear-to-r from-green-200 to-green-100
|
||||
bg-[length:0px_10px] bg-left-bottom bg-no-repeat
|
||||
transition-[background-size]
|
||||
duration-500
|
||||
@ -66,15 +71,15 @@ export default function BlogCard({ post }: BlogCardProps) {
|
||||
group-hover:bg-[length:100%_10px]
|
||||
dark:from-purple-800 dark:to-purple-900"
|
||||
>
|
||||
{post.title}
|
||||
{title}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
{/* Post excerpt */}
|
||||
<div className="mt-2">
|
||||
{post.description && (
|
||||
{description && (
|
||||
<p className="line-clamp-2 text-sm text-muted-foreground">
|
||||
{post.description}
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@ -84,20 +89,20 @@ export default function BlogCard({ post }: BlogCardProps) {
|
||||
<div className="mt-4 pt-4 border-t flex items-center justify-between space-x-4 text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-8 w-8 shrink-0">
|
||||
{post?.author?.avatar && (
|
||||
{blogAuthor?.data.avatar && (
|
||||
<Image
|
||||
src={post?.author?.avatar}
|
||||
alt={`avatar for ${post?.author?.name}`}
|
||||
src={blogAuthor?.data.avatar}
|
||||
alt={`avatar for ${blogAuthor?.data.name}`}
|
||||
className="rounded-full object-cover border"
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="truncate text-sm">{post?.author?.name}</span>
|
||||
<span className="truncate text-sm">{blogAuthor?.data.name}</span>
|
||||
</div>
|
||||
|
||||
<time className="truncate text-sm" dateTime={date}>
|
||||
{date}
|
||||
{publishDate}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import Container from '@/components/layout/container';
|
||||
import type { Category } from 'content-collections';
|
||||
import type { BlogCategory } from '@/types/blog-types';
|
||||
import { BlogCategoryListDesktop } from './blog-category-list-desktop';
|
||||
import { BlogCategoryListMobile } from './blog-category-list-mobile';
|
||||
|
||||
interface BlogCategoryFilterProps {
|
||||
categoryList: Category[];
|
||||
categoryList: BlogCategory[];
|
||||
}
|
||||
|
||||
export function BlogCategoryFilter({ categoryList }: BlogCategoryFilterProps) {
|
||||
|
@ -3,12 +3,12 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Category } from 'content-collections';
|
||||
import type { BlogCategory } from '@/types/blog-types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export type BlogCategoryListDesktopProps = {
|
||||
categoryList: Category[];
|
||||
categoryList: BlogCategory[];
|
||||
};
|
||||
|
||||
export function BlogCategoryListDesktop({
|
||||
|
@ -9,14 +9,14 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from '@/components/ui/drawer';
|
||||
import type { Category } from 'content-collections';
|
||||
import type { BlogCategory } from '@/types/blog-types';
|
||||
import { LayoutListIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
export type BlogCategoryListMobileProps = {
|
||||
categoryList: Category[];
|
||||
categoryList: BlogCategory[];
|
||||
};
|
||||
|
||||
export function BlogCategoryListMobile({
|
||||
|
@ -1,15 +1,17 @@
|
||||
import type { Post } from 'content-collections';
|
||||
import type { BlogType } from '@/lib/docs/source';
|
||||
import EmptyGrid from '../shared/empty-grid';
|
||||
import CustomPagination from '../shared/pagination';
|
||||
import BlogGrid from './blog-grid';
|
||||
|
||||
interface BlogGridWithPaginationProps {
|
||||
posts: Post[];
|
||||
locale: string;
|
||||
posts: BlogType[];
|
||||
totalPages: number;
|
||||
routePrefix: string;
|
||||
}
|
||||
|
||||
export default function BlogGridWithPagination({
|
||||
locale,
|
||||
posts,
|
||||
totalPages,
|
||||
routePrefix,
|
||||
@ -19,7 +21,7 @@ export default function BlogGridWithPagination({
|
||||
{posts.length === 0 && <EmptyGrid />}
|
||||
{posts.length > 0 && (
|
||||
<div>
|
||||
<BlogGrid posts={posts} />
|
||||
<BlogGrid locale={locale} posts={posts} />
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<CustomPagination
|
||||
routePrefix={routePrefix}
|
||||
|
@ -1,19 +1,20 @@
|
||||
import BlogCard, { BlogCardSkeleton } from '@/components/blog/blog-card';
|
||||
import { websiteConfig } from '@/config/website';
|
||||
import type { Post } from 'content-collections';
|
||||
import type { BlogType } from '@/lib/docs/source';
|
||||
|
||||
interface BlogGridProps {
|
||||
posts: Post[];
|
||||
locale: string;
|
||||
posts: BlogType[];
|
||||
}
|
||||
|
||||
export default function BlogGrid({ posts }: BlogGridProps) {
|
||||
export default function BlogGrid({ locale, posts }: BlogGridProps) {
|
||||
// console.log('BlogGrid, posts', posts);
|
||||
return (
|
||||
<div>
|
||||
{posts?.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{posts.map((post) => (
|
||||
<BlogCard key={post.slug} post={post} />
|
||||
<BlogCard key={post.slugs[0]} locale={locale} post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
25
src/types/blog-types.ts
Normal file
25
src/types/blog-types.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Blog Category
|
||||
*
|
||||
* we can not pass CategoryType from server component to client component
|
||||
* so we need to define a new type, and use it in the client component
|
||||
*/
|
||||
export type BlogCategory = {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type BlogAuthor = {
|
||||
slug: string;
|
||||
name: string;
|
||||
description: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
export type BlogPost = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
};
|
Loading…
Reference in New Issue
Block a user