diff options
Diffstat (limited to 'src/app/(main)/(home)/_components')
| -rw-r--r-- | src/app/(main)/(home)/_components/call-to-action.tsx | 23 | ||||
| -rw-r--r-- | src/app/(main)/(home)/_components/hero.tsx | 98 | ||||
| -rw-r--r-- | src/app/(main)/(home)/_components/posts.tsx | 89 |
3 files changed, 210 insertions, 0 deletions
diff --git a/src/app/(main)/(home)/_components/call-to-action.tsx b/src/app/(main)/(home)/_components/call-to-action.tsx new file mode 100644 index 0000000..b75298e --- /dev/null +++ b/src/app/(main)/(home)/_components/call-to-action.tsx @@ -0,0 +1,23 @@ +import { NewsletterForm } from '@/components/newsletter-form'; +import { Section } from '@/components/section'; +import type React from 'react'; + +export function CTA(): React.ReactElement { + return ( + <Section className='relative grid gap-8 px-4 py-10 sm:grid-cols-2 md:py-14 lg:px-6 lg:py-16'> + <div className='max-w-xl space-y-2'> + <h2 className='font-semibold text-2xl md:text-3xl lg:text-4xl'> + Subscribe to the Newsletter + </h2> + <p className='text-muted-foreground text-sm md:text-base'> + Get the latest articles and updates delivered straight to your inbox. + No spam, unsubscribe anytime. + </p> + </div> + + <div className='flex w-full items-center'> + <NewsletterForm /> + </div> + </Section> + ); +} diff --git a/src/app/(main)/(home)/_components/hero.tsx b/src/app/(main)/(home)/_components/hero.tsx new file mode 100644 index 0000000..04371ca --- /dev/null +++ b/src/app/(main)/(home)/_components/hero.tsx @@ -0,0 +1,98 @@ +import { baseOptions, linkItems } from '@/app/(main)/layout.config'; +import { Icons } from '@/components/icons/icons'; +import { Section } from '@/components/section'; +import { buttonVariants } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { getLinks } from 'fumadocs-ui/layouts/shared'; +import * as motion from 'motion/react-client'; +import Image from 'next/image'; +import Link from 'next/link'; +import Balancer from 'react-wrap-balancer'; +import heroImage from '../../../../../public/images/gradient-noise-purple-azure-light.png'; + +const Hero = () => { + const links = getLinks(linkItems, baseOptions.githubUrl); + const navItems = links.filter((item) => + ['nav', 'all'].includes(item.on ?? 'all'), + ); + + return ( + <Section className='relative flex flex-col items-center justify-center gap-6 overflow-hidden bg-dashed px-4 py-16 sm:px-16 sm:py-24 md:py-32'> + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ + duration: 0.4, + scale: { type: 'spring', visualDuration: 0.4, bounce: 0.5 }, + }} + whileInView={{ opacity: 1 }} + viewport={{ once: true }} + className='-z-10 absolute inset-0 h-full w-full' + > + <Image + src={heroImage} + alt='Hero Background' + height={600} + width={704} + className='pointer-events-none absolute right-0 bottom-0 h-[900px] w-[1004px] max-w-[1004px] translate-x-1/2 translate-y-1/2 select-none opacity-80 dark:opacity-100' + priority + /> + </motion.div> + <div className='flex items-center justify-center space-x-2'> + <Icons.code className='h-6 w-6 text-primary transition-transform hover:scale-125' /> + <span className='font-medium text-muted-foreground text-sm'> + Full-Stack Developer & Tech Writer + </span> + </div> + <h1 className='max-w-3xl text-center font-bold text-4xl leading-tight tracking-tighter sm:text-5xl md:max-w-4xl md:text-6xl lg:leading-[1.1]'> + <Balancer>I'm John Doe , a Full-Stack Developer.</Balancer> + </h1> + <p className='max-w-xl text-center text-muted-foreground md:max-w-2xl md:text-lg'> + <Balancer> + I write about web development, software engineering, and the latest + technologies. I also create fun projects and tutorials to help you + learn and grow as a developer. + </Balancer> + </p> + + <div className='flex flex-wrap items-center justify-center gap-4'> + <Link + className={cn( + buttonVariants({ + variant: 'default', + size: 'lg', + }), + 'group rounded-full bg-primary hover:bg-primary/90', + )} + href='/posts' + > + Browse Posts + <Icons.arrowUpRight className='group-hover:-rotate-12 ml-2 size-5 transition-transform' /> + </Link> + + <div className='flex items-center space-x-4'> + {navItems + .filter((item) => item.type === 'icon') + .map((item, i) => ( + <Link + key={i.toString()} + href={item.url} + className={cn( + buttonVariants({ + variant: 'ghost', + size: 'icon', + }), + 'rounded-full', + )} + > + {item.icon} + <span className='sr-only'>{item.text}</span> + </Link> + ))} + </div> + </div> + </Section> + ); +}; + +export default Hero; diff --git a/src/app/(main)/(home)/_components/posts.tsx b/src/app/(main)/(home)/_components/posts.tsx new file mode 100644 index 0000000..00ada0c --- /dev/null +++ b/src/app/(main)/(home)/_components/posts.tsx @@ -0,0 +1,89 @@ +import { Icons } from '@/components/icons/icons'; +import { PostCard } from '@/components/posts/post-card'; +import { Section } from '@/components/section'; +import { buttonVariants } from '@/components/ui/button'; +import type { Page } from '@/lib/source'; +import type { BlogPost } from '@/lib/payload-posts'; +import Link from 'next/link'; + +// 统一的文章数据格式 +interface UnifiedPost { + title: string; + description: string; + image?: string | null; + url: string; + date: string; + author: string; + tags?: string[]; +} + +// 将 MDX Page 转换为统一格式 +function transformMdxPost(post: Page): UnifiedPost { + return { + title: post.data.title, + description: post.data.description ?? '', + image: post.data.image, + url: post.url, + date: new Date(post.data.date).toDateString(), + author: post.data.author, + tags: post.data.tags, + }; +} + +// 将 Payload BlogPost 转换为统一格式 +function transformPayloadPost(post: BlogPost): UnifiedPost { + return { + title: post.title, + description: post.description, + image: post.image, + url: post.url, + date: post.date.toDateString(), + author: post.author, + tags: post.tags, + }; +} + +interface PostsProps { + mdxPosts?: Page[]; + payloadPosts?: BlogPost[]; +} + +export default function Posts({ mdxPosts = [], payloadPosts = [] }: PostsProps) { + // 转换并合并所有文章 + const allPosts: UnifiedPost[] = [ + ...mdxPosts.map(transformMdxPost), + ...payloadPosts.map(transformPayloadPost), + ]; + + // 按日期排序 + allPosts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + + return ( + <Section> + <div className='grid divide-y divide-dashed divide-border/70 text-left dark:divide-border'> + {allPosts.map((post) => ( + <PostCard + title={post.title} + description={post.description} + image={post.image} + url={post.url} + date={post.date} + key={post.url} + author={post.author} + tags={post.tags} + /> + ))} + <Link + href='/posts' + className={buttonVariants({ + variant: 'default', + className: 'group rounded-none py-4 sm:py-8', + })} + > + View More + <Icons.arrowUpRight className='group-hover:-rotate-12 ml-2 size-5 transition-transform' /> + </Link> + </div> + </Section> + ); +} |
