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