mirror of
https://github.com/Lumither/nocturne.git
synced 2026-10-17 10:26:21 +00:00
feat(frontend) new tag filter, new landing page
This commit is contained in:
18 files changed
+256
-427
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const ToolBar = () => {
|
||||
return (
|
||||
<div className={ 'w-full' }>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolBar;
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user