summaryrefslogtreecommitdiff
path: root/src/app/(home)/posts
diff options
context:
space:
mode:
authorBertrand Yuan <bert.yuan@outlook.com>2025-12-16 00:12:49 +0800
committerBertrand Yuan <bert.yuan@outlook.com>2025-12-16 00:12:49 +0800
commit02ae938c238c9d18448d17a8ec92c0edd8c17463 (patch)
treedcd6a30505adb52522b20af2c0ac27f713403f10 /src/app/(home)/posts
parent48b07bc308a35734a6a7a305c8fdccbfa47de7d8 (diff)
feat(back-end): introduce payload
Payload is the next.js Headless CMS and App Framework, I would like to pick it up and modify it as it is MIT licensed. Many features in Payload is not applicable for our project. So, I modify it so that it is light and clear.
Diffstat (limited to 'src/app/(home)/posts')
-rw-r--r--src/app/(home)/posts/[slug]/page.client.tsx57
-rw-r--r--src/app/(home)/posts/[slug]/page.tsx145
-rw-r--r--src/app/(home)/posts/page.tsx133
3 files changed, 0 insertions, 335 deletions
diff --git a/src/app/(home)/posts/[slug]/page.client.tsx b/src/app/(home)/posts/[slug]/page.client.tsx
deleted file mode 100644
index 7a97f56..0000000
--- a/src/app/(home)/posts/[slug]/page.client.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-import {
- UploadIcon as ShareIcon,
- type UploadIconHandle as ShareIconHandle,
-} from '@/components/icons/animated/upload';
-import { Icons } from '@/components/icons/icons';
-import { Button } from '@/components/ui/button';
-import { cn } from '@/lib/utils';
-import { Comments } from '@fuma-comment/react';
-import { redirect } from 'next/navigation';
-import { useRef } from 'react';
-import { toast } from 'sonner';
-import { useCopyToClipboard } from 'usehooks-ts';
-
-export function Share({ url }: { url: string }): React.ReactElement {
- const iconRef = useRef<ShareIconHandle>(null);
- const [_, copyToClipboard] = useCopyToClipboard();
-
- const onClick = async (): Promise<void> => {
- await copyToClipboard(`${window.location.origin}${url}`);
- toast.success('Copied to clipboard!', {
- icon: <Icons.copied className='size-4' />,
- description: 'The post link has been copied to your clipboard.',
- });
- };
-
- return (
- <Button
- className={cn('group gap-2')}
- variant={'secondary'}
- onClick={onClick}
- onMouseEnter={() => iconRef.current?.startAnimation?.()}
- onMouseLeave={() => iconRef.current?.stopAnimation?.()}
- >
- <ShareIcon className='size-4 hover:bg-transparent' ref={iconRef} />
- Share Post
- </Button>
- );
-}
-
-export function PostComments({
- slug,
- className,
-}: { slug: string; className?: string }) {
- return (
- <Comments
- page={slug}
- className={cn('w-full', className)}
- auth={{
- type: 'api',
- signIn: () => {
- redirect('/login');
- },
- }}
- />
- );
-}
diff --git a/src/app/(home)/posts/[slug]/page.tsx b/src/app/(home)/posts/[slug]/page.tsx
deleted file mode 100644
index 15a6bfd..0000000
--- a/src/app/(home)/posts/[slug]/page.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { PostComments, Share } from '@/app/(home)/posts/[slug]/page.client';
-import { PostJsonLd } from '@/components/json-ld';
-import { Section } from '@/components/section';
-import { TagCard } from '@/components/tags/tag-card';
-import { createMetadata } from '@/lib/metadata';
-import { metadataImage } from '@/lib/metadata-image';
-import { type Page as MDXPage, getPost, getPosts } from '@/lib/source';
-import { cn } from '@/lib/utils';
-import { File, Files, Folder } from 'fumadocs-ui/components/files';
-import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
-import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
-import defaultMdxComponents from 'fumadocs-ui/mdx';
-import type { Metadata } from 'next';
-import { notFound } from 'next/navigation';
-import Balancer from 'react-wrap-balancer';
-import { description as homeDescription } from 'src/app/layout.config';
-
-function Header(props: { page: MDXPage; tags?: string[] }) {
- const { page, tags } = props;
-
- return (
- <Section className='p-4 lg:p-6'>
- <div
- className={cn(
- 'flex flex-col items-start justify-center gap-4 py-8 md:gap-6',
- 'sm:items-center sm:rounded-lg sm:border sm:bg-muted/70 sm:px-8 sm:py-20 sm:shadow-xs sm:dark:bg-muted',
- )}
- >
- <div className='flex flex-col gap-2 sm:text-center md:gap-4'>
- <h1 className='max-w-4xl font-bold text-3xl leading-tight tracking-tight sm:text-4xl sm:leading-tight md:text-5xl md:leading-tight'>
- <Balancer>{page.data.title}</Balancer>
- </h1>
- <p className='mx-auto max-w-4xl'>
- <Balancer>{page.data.description}</Balancer>
- </p>
- </div>
- <div className='flex flex-wrap gap-2'>
- {tags?.map((tag) => (
- <TagCard name={tag} key={tag} className=' border border-border ' />
- ))}
- </div>
- </div>
- </Section>
- );
-}
-
-export default async function Page(props: {
- params: Promise<{ slug: string }>;
-}) {
- const params = await props.params;
- const page = getPost([params.slug]);
-
- if (!page) notFound();
- const { body: Mdx, toc, tags, lastModified } = page.data;
-
- const lastUpdate = lastModified ? new Date(lastModified) : undefined;
-
- return (
- <>
- <Header page={page} tags={tags} />
-
- <Section className='h-full' sectionClassName='flex flex-1'>
- <article className='flex min-h-full flex-col lg:flex-row'>
- <div className='flex flex-1 flex-col gap-4'>
- <InlineTOC
- items={toc}
- className='rounded-none border-0 border-border/70 border-b border-dashed dark:border-border'
- />
- <div className='prose min-w-0 flex-1 px-4'>
- <Mdx
- components={{
- ...defaultMdxComponents,
- File,
- Files,
- Folder,
- Tabs,
- Tab,
- }}
- />
- </div>
- <PostComments
- slug={params.slug}
- className='[&_form>div]:!rounded-none rounded-none border-0 border-border/70 border-t border-dashed dark:border-border'
- />
- </div>
- <div className='flex flex-col gap-4 p-4 text-sm lg:sticky lg:top-[4rem] lg:h-[calc(100vh-4rem)] lg:w-[250px] lg:self-start lg:overflow-y-auto lg:border-border/70 lg:border-l lg:border-dashed lg:dark:border-border'>
- <div>
- <p className='mb-1 text-fd-muted-foreground'>Written by</p>
- <p className='font-medium'>{page.data.author}</p>
- </div>
- <div>
- <p className='mb-1 text-fd-muted-foreground text-sm'>
- Created At
- </p>
- <p className='font-medium'>
- {new Date(page.data.date ?? page.file.name).toDateString()}
- </p>
- </div>
- {lastUpdate && (
- <div>
- <p className='mb-1 text-fd-muted-foreground text-sm'>
- Updated At
- </p>
- <p className='font-medium'>{lastUpdate.toDateString()}</p>
- </div>
- )}
- <Share url={page.url} />
- </div>
- </article>
- </Section>
- <PostJsonLd page={page} />
- </>
- );
-}
-
-export async function generateMetadata(props: {
- params: Promise<{ slug: string }>;
-}): Promise<Metadata> {
- const params = await props.params;
- const page = getPost([params.slug]);
-
- if (!page) notFound();
-
- const title = page.data.title;
- const description = page.data.description ?? homeDescription;
-
- return createMetadata(
- metadataImage.withImage(page.slugs, {
- title,
- description,
- openGraph: {
- url: `/posts/${page.slugs.join('/')}`,
- },
- alternates: {
- canonical: page.url,
- },
- }),
- );
-}
-
-export function generateStaticParams(): { slug: string | undefined }[] {
- return getPosts().map((page) => ({
- slug: page.slugs[0],
- }));
-}
diff --git a/src/app/(home)/posts/page.tsx b/src/app/(home)/posts/page.tsx
deleted file mode 100644
index fd0f912..0000000
--- a/src/app/(home)/posts/page.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import { postsPerPage } from '@/app/layout.config';
-import { NumberedPagination } from '@/components/numbered-pagination';
-import { PostCard } from '@/components/posts/post-card';
-import { Section } from '@/components/section';
-import { createMetadata } from '@/lib/metadata';
-import { getSortedByDatePosts } from '@/lib/source';
-import type { Metadata, ResolvingMetadata } from 'next';
-import { notFound, redirect } from 'next/navigation';
-
-export const dynamicParams = false;
-
-const totalPosts = getSortedByDatePosts().length;
-const pageCount = Math.ceil(totalPosts / postsPerPage);
-
-const CurrentPostsCount = ({
- startIndex,
- endIndex,
-}: {
- startIndex: number;
- endIndex: number;
-}) => {
- const start = startIndex + 1;
- const end = endIndex < totalPosts ? endIndex : totalPosts;
- if (start === end) return <span>({start})</span>;
- return (
- <span>
- ({start}-{end})
- </span>
- );
-};
-
-const Pagination = ({ pageIndex }: { pageIndex: number }) => {
- const handlePageChange = async (page: number) => {
- 'use server';
- redirect(`/posts?page=${page}`);
- };
-
- return (
- <Section className='bg-dashed'>
- <NumberedPagination
- currentPage={pageIndex + 1}
- totalPages={pageCount}
- paginationItemsToDisplay={5}
- onPageChange={handlePageChange}
- />
- </Section>
- );
-};
-
-export default async function Page(props: {
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-}) {
- const searchParams = await props.searchParams;
- const pageIndex = searchParams.page
- ? Number.parseInt(searchParams.page[0] ?? '', 10) - 1
- : 0;
- if (pageIndex < 0 || pageIndex >= pageCount) notFound();
-
- const startIndex = pageIndex * postsPerPage;
- const endIndex = startIndex + postsPerPage;
- const posts = getSortedByDatePosts().slice(startIndex, endIndex);
-
- return (
- <>
- <Section className='p-4 lg:p-6'>
- <h1 className='font-bold text-3xl leading-tight tracking-tighter md:text-4xl'>
- All {totalPosts} Posts{' '}
- <CurrentPostsCount startIndex={startIndex} endIndex={endIndex} />
- </h1>
- </Section>
- <Section className='h-full' sectionClassName='flex flex-1'>
- <div className='grid divide-y divide-dashed divide-border/70 text-left dark:divide-border'>
- {posts.map((post) => {
- const date = new Date(post.data.date).toDateString();
- return (
- <PostCard
- title={post.data.title}
- description={post.data.description ?? ''}
- image={post.data.image}
- url={post.url}
- date={date}
- key={post.url}
- author={post.data.author}
- tags={post.data.tags}
- />
- );
- })}
- </div>
- </Section>
- {pageCount > 1 && <Pagination pageIndex={pageIndex} />}
- </>
- );
-}
-
-export const generateStaticParams = () => {
- const slugs = Array.from({ length: pageCount }, (_, index) => ({
- slug: [(index + 1).toString()],
- }));
-
- return [{ slug: [] }, ...slugs];
-};
-
-type Props = {
- params: Promise<{ slug: string[] }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-};
-
-export async function generateMetadata(
- props: Props,
- parent: ResolvingMetadata,
-): Promise<Metadata> {
- const params = await props.params;
- const searchParams = await props.searchParams;
-
- const pageIndex = searchParams.page
- ? Number.parseInt(searchParams.page as string, 10)
- : 1;
-
- const isFirstPage = pageIndex === 1 || !searchParams.page;
- const pageTitle = isFirstPage ? 'Posts' : `Posts - Page ${pageIndex}`;
- const canonicalUrl = isFirstPage ? '/posts' : `/posts?page=${pageIndex}`;
-
- return createMetadata({
- title: pageTitle,
- description: `Posts${!isFirstPage ? ` - Page ${pageIndex}` : ''}`,
- openGraph: {
- url: canonicalUrl,
- },
- alternates: {
- canonical: canonicalUrl,
- },
- });
-}