feat(frontend) new tag filter, new landing page

This commit is contained in:
lumither committed 2025-10-13 02:59:21 -04:00
1 parent 314f5780fd
commit 02e7166fcb
18 files changed
+256 -427

No files matched your search

+32 -11
View File
@@ -4,6 +4,8 @@ import { Chip } from '@nextui-org/react';
import Image from 'next/image';
import Link from 'next/link';
import { MdCalendarMonth, MdNorthEast } from 'react-icons/md';
import { HighlightSelector } from '@/app/(pages)/blog/post/components/AnimationPostList';
import TagChip from '@/app/(pages)/blog/TagChip';
interface Post {
id: string,
@@ -19,15 +21,20 @@ interface Post {
type Props = {
post: Post
highlightSelector: HighlightSelector,
}
function BlogPostCard({ post }: Props) {
function BlogPostCard({ post, highlightSelector }: Props) {
const highlightTags = highlightSelector?.tags;
return (
<div className={ `w-full` }>
<Card as={ Link } href={ `/blog/post/${ post.identifier }` }
className={ `group` }
isPressable
isHoverable
<Card
as={ Link } href={ `/blog/post/${ post.identifier }` }
className={ `group w-full` }
isPressable
isHoverable
>
{ post.header_img && <div className={ `w-full overflow h-80 relative` }>
@@ -63,12 +70,26 @@ function BlogPostCard({ post }: Props) {
<div className={ `flex flex-row flex-wrap justify-between w-full` }>
<div>
<ul className={ `flex flex-row flex-wrap` }>
{
post.tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
</li>
))
{/*highlight*/
post.tags
.filter((tag) => highlightTags?.includes(tag))
.sort()
.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<TagChip tagName={ tag } isHighlighted={ true } />
</li>
))
}
{/*normal */
post.tags
.filter((tag) => !highlightTags?.includes(tag))
.sort()
.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<TagChip tagName={ tag } isHighlighted={ false } />
</li>
))
}
</ul>
</div>
+7 -2
View File
@@ -8,7 +8,9 @@ import { useScreenSizeTrigger } from '@/app/(pages)/public/screenSizeTrigger';
const Pagination = ({ total }: {
total: number,
}) => {
const parsePage = Number(useSearchParams().get('page'));
const searchParams = useSearchParams();
const parsePage = Number(searchParams.get('page'));
const currPage = parsePage > 0 ? parsePage : 1;
const router = useRouter();
@@ -27,7 +29,10 @@ const Pagination = ({ total }: {
color={ 'default' }
variant={ 'light' }
onChange={ (page) => {
router.push(`?page=${ page }`, { scroll: false });
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
const queryString = params.toString();
router.push(`?${ queryString }`, { scroll: false });
} }
/>
</div>
+11 -11
View File
@@ -1,19 +1,19 @@
'use server';
import React from 'react';
import { fetchPostList } from '@/src/api/blog/post';
import AnimationPostList from '@/app/(pages)/blog/post/components/AnimationPostList';
import { Post } from '@/src/api/blog/post';
import AnimationPostList, { HighlightSelector } from '@/app/(pages)/blog/post/components/AnimationPostList';
const PostList = async ({ page }: { page: number }) => {
interface Props {
posts: Post[],
tags: string[] | undefined
}
const PostList = async ({ posts, tags }: Props) => {
try {
const postListResponse = await fetchPostList(page);
const posts = postListResponse.data.posts;
return (
// blog list
<div className={ 'w-full' }>
<AnimationPostList posts={ posts } />
<AnimationPostList posts={ posts } highlight={ {
tags: tags
} } />
</div>
);
} catch (e: any) {
+48
View File
@@ -0,0 +1,48 @@
'use client';
import React from 'react';
import { Chip } from '@nextui-org/react';
import { useRouter, useSearchParams } from 'next/navigation';
interface TagChipProps {
tagName: string,
isHighlighted: boolean
}
const TagChip = ({ tagName, isHighlighted }: TagChipProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const toggleTag = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
const params = new URLSearchParams(searchParams.toString());
const tags = params.getAll('tags');
if (tags.includes(tagName)) {
const newTags = tags.filter(t => t !== tagName);
params.delete('tags');
newTags.forEach(t => params.append('tags', t));
} else {
params.append('tags', tagName);
}
const queryString = params.toString();
router.push(`?${ queryString }`, { scroll: false });
};
return (
<div
onClick={ toggleTag }
className="cursor-pointer relative"
>
<Chip
className={ 'hover:border-amber-400 hover:dark:border-amber-700 transition-all' }
variant={ 'bordered' }
color={ isHighlighted ? 'warning' : 'default' }
>{ tagName }</Chip>
</div>
);
};
export default TagChip;
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
const ToolBar = () => {
return (
<div className={ 'w-full' }>
</div>
);
};
export default ToolBar;
+30 -7
View File
@@ -1,6 +1,7 @@
import PostList from '@/app/(pages)/blog/PostList';
import Pagination from '@/app/(pages)/blog/Pagination';
import { fetchPostListPagination } from '@/src/api/blog/post';
import { fetchPostList } from '@/src/api/blog/post';
import ToolBar from '@/app/(pages)/blog/ToolBar';
async function Blog({
@@ -9,18 +10,40 @@ async function Blog({
searchParams: Promise<{ [_: string]: string | string[] | undefined }>
}) {
const parsePage = Number((await searchParams)?.page);
const sp = await searchParams;
const parsePage = Number(sp?.page);
const currPage = parsePage > 0 ? parsePage : 1;
const matchAll = Boolean(sp?.match_all);
const ensureList = (input: string | string[] | undefined): string[] | undefined => {
if (!input) {
return undefined;
}
if (typeof input === 'string') {
return [ input ];
} else {
return input;
}
};
const tags = ensureList(sp?.tags);
try {
const pagination = await fetchPostListPagination();
const pageCount = pagination.data.page_count;
const postListResponse = await fetchPostList({
page: currPage,
match_all: matchAll,
tag_filter: tags
});
const pagination = postListResponse.data.pagination;
const posts = postListResponse.data.posts;
return (
<div className={ 'w-full min-w-full space-y-4' }>
<PostList page={ currPage } />
<Pagination total={ pageCount } />
<ToolBar />
<PostList posts={ posts } tags={ tags } />
<Pagination total={ pagination.page_count } />
</div>
);
} catch (e: any) {
@@ -28,7 +51,7 @@ async function Blog({
<div className={ `flex h-full min-h-dvh w-full justify-center items-center` }>
<div className={ `flex-row` }>
<p className={ `text-xl font-bold` }>
{ 'Unexpected Error [loading pagination]:' }
{ 'Unexpected Error [loading blog posts]:' }
</p>
<p className={ `font-bold` }>
{ e.message }
@@ -2,6 +2,7 @@ import React from 'react';
import Image from 'next/image';
import { Chip } from '@nextui-org/react';
import { MdCalendarMonth } from 'react-icons/md';
import TagChip from '@/app/(pages)/blog/post/[id]/TagChip';
type Props = {
title: string;
@@ -50,7 +51,7 @@ const Headers = (post: Props) => {
{
post.tags.map((tag: string, key: any) => (
<li key={ key } className={ `mx-1 mb-1` }>
<Chip>{ tag }</Chip>
<TagChip tag={ tag } />
</li>
))
}
@@ -0,0 +1,20 @@
'use client';
import React from 'react';
import { Chip } from '@nextui-org/react';
import { useRouter } from 'next/navigation';
const TagChip = ({ tag }: { tag: string }) => {
const router = useRouter();
return (
<div onClick={ () => router.push(`/blog?tags=${ tag }`) }>
<Chip
className={ 'hover:border-amber-400 hover:dark:border-amber-700 transition-all' }
variant={ 'faded' }
>{ tag }</Chip>
</div>
);
};
export default TagChip;
@@ -7,7 +7,16 @@ import * as motion from 'motion/react-client';
import { AnimatePresence } from 'motion/react';
import { Post } from '@/src/api/blog/post';
const PostList = ({ posts }: { posts: Post[] }) => {
export interface HighlightSelector {
tags?: string[];
}
interface Props {
posts: Post[];
highlight: HighlightSelector;
}
const PostList = ({ posts, highlight }: Props) => {
return (
<div className={ 'w-full' }>
<ul className={ 'flex flex-col items-center w-full space-y-7' }>
@@ -19,7 +28,7 @@ const PostList = ({ posts }: { posts: Post[] }) => {
transition={ { ease: 'easeOut', duration: 0.5, delay: key * .1 + .1 } }
key={ key }
className={ 'w-full' }>
<BlogPostCard post={ post } />
<BlogPostCard post={ post } highlightSelector={ highlight } />
</motion.li>)
) }
</AnimatePresence>