Files
soul-yongping/app/page.tsx
v0 d5df83f35b feat: refactor homepage and navigation structure
Remove unused pages, redesign author page, update homepage with new chapter display, and modify bottom navigation.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
2026-01-14 06:39:23 +00:00

275 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect, useRef, useCallback } from "react"
import { useRouter } from "next/navigation"
import { ChevronRight, Sparkles, Lock, Share2 } from "lucide-react"
import { useStore } from "@/lib/store"
import { getFullBookPrice, getTotalSectionCount } from "@/lib/book-data"
import { AuthModal } from "@/components/modules/auth/auth-modal"
import { PaymentModal } from "@/components/payment-modal"
export default function HomePage() {
const router = useRouter()
const { user, isLoggedIn, hasPurchased } = useStore()
const [content, setContent] = useState<string>("")
const [isLoading, setIsLoading] = useState(true)
const [showPaywall, setShowPaywall] = useState(false)
const [readingProgress, setReadingProgress] = useState(0)
const [isAuthOpen, setIsAuthOpen] = useState(false)
const [isPaymentOpen, setIsPaymentOpen] = useState(false)
const [paymentType, setPaymentType] = useState<"section" | "fullbook">("section")
const contentRef = useRef<HTMLDivElement>(null)
const fullBookPrice = getFullBookPrice()
const totalSections = getTotalSectionCount()
const hasFullBook = user?.hasFullBook || false
// 最新章节 - 使用1.1作为示例
const latestSection = {
id: "1.1",
title: "荷包:电动车出租的被动收入模式",
price: 1,
isFree: true,
filePath: "book/第一篇|真实的人/第1章人与人之间的底层逻辑/1.1 荷包:电动车出租的被动收入模式.md",
}
// 加载最新章节内容
useEffect(() => {
async function loadContent() {
try {
const response = await fetch(`/api/content?path=${encodeURIComponent(latestSection.filePath)}`)
if (response.ok) {
const data = await response.json()
setContent(data.content || "")
}
} catch (error) {
console.error("Failed to load content:", error)
} finally {
setIsLoading(false)
}
}
loadContent()
}, [])
// 监听滚动进度
const handleScroll = useCallback(() => {
if (!contentRef.current) return
const scrollTop = window.scrollY
const docHeight = document.documentElement.scrollHeight - window.innerHeight
const progress = docHeight > 0 ? Math.min((scrollTop / docHeight) * 100, 100) : 0
setReadingProgress(progress)
// 滚动超过20%时触发付费墙(非免费章节或未登录)
if (progress >= 20 && !hasFullBook && !latestSection.isFree) {
setShowPaywall(true)
}
}, [hasFullBook])
useEffect(() => {
window.addEventListener("scroll", handleScroll)
return () => window.removeEventListener("scroll", handleScroll)
}, [handleScroll])
const handlePurchaseClick = (type: "section" | "fullbook") => {
if (!isLoggedIn) {
setIsAuthOpen(true)
return
}
setPaymentType(type)
setIsPaymentOpen(true)
}
const contentLines = content.split("\n").filter((line) => line.trim())
// 如果需要付费墙只显示前20%内容
const displayContent =
showPaywall && !hasFullBook && !latestSection.isFree
? contentLines.slice(0, Math.ceil(contentLines.length * 0.2)).join("\n")
: content
return (
<div className="min-h-screen bg-black text-white pb-24">
{/* 阅读进度条 */}
<div className="fixed top-0 left-0 right-0 z-50 h-0.5 bg-[#1c1c1e]">
<div
className="h-full bg-gradient-to-r from-[#ff3b5c] to-[#ff6b8a] transition-all duration-150"
style={{ width: `${readingProgress}%` }}
/>
</div>
{/* 顶部标题区 */}
<header className="sticky top-0 z-40 bg-black/90 backdrop-blur-xl border-b border-white/5">
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[#ffd700] text-lg font-semibold">Soul派对·</span>
<span className="text-[10px] text-[#ff3b5c] bg-[#ff3b5c]/10 px-2 py-0.5 rounded"></span>
</div>
<button
onClick={() => {
const url = window.location.href
navigator.clipboard.writeText(url)
alert("链接已复制")
}}
className="w-8 h-8 rounded-full bg-[#1c1c1e] flex items-center justify-center"
>
<Share2 className="w-4 h-4 text-gray-400" />
</button>
</div>
</div>
</header>
{/* 章节标题 */}
<div className="px-4 pt-6 pb-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-[#ff3b5c] text-sm font-medium bg-[#ff3b5c]/10 px-3 py-1 rounded-full">
{latestSection.id}
</span>
{latestSection.isFree && (
<span className="text-xs text-[#00E5FF] bg-[#00E5FF]/10 px-2 py-0.5 rounded"></span>
)}
</div>
<h1 className="text-xl font-bold text-white leading-tight">{latestSection.title}</h1>
<p className="text-gray-500 text-sm mt-2"></p>
</div>
{/* 内容区域 */}
<main ref={contentRef} className="px-4 pb-8">
{isLoading ? (
<div className="space-y-4">
{[...Array(8)].map((_, i) => (
<div
key={i}
className="h-4 bg-[#1c1c1e] rounded animate-pulse"
style={{ width: `${Math.random() * 40 + 60}%` }}
/>
))}
</div>
) : (
<article className="text-gray-300 leading-[1.9] text-[16px]">
{displayContent.split("\n").map(
(paragraph, index) =>
paragraph.trim() && (
<p key={index} className="mb-5">
{paragraph}
</p>
),
)}
</article>
)}
{/* 付费墙 - 只在非免费章节且滚动超过20%时显示 */}
{showPaywall && !hasFullBook && !latestSection.isFree && (
<div className="relative mt-4">
{/* 渐变遮罩 */}
<div className="absolute -top-32 left-0 right-0 h-32 bg-gradient-to-t from-black to-transparent pointer-events-none" />
{/* 付费卡片 */}
<div className="p-6 rounded-2xl bg-gradient-to-b from-[#1c1c1e] to-[#2c2c2e] border border-white/10">
<div className="text-center">
<div className="w-14 h-14 mx-auto mb-3 rounded-2xl bg-[#ff3b5c]/10 flex items-center justify-center">
<Lock className="w-7 h-7 text-[#ff3b5c]" />
</div>
<h3 className="text-lg font-semibold text-white mb-2"></h3>
<p className="text-gray-400 text-sm mb-5">{isLoggedIn ? "支付1元继续阅读" : "登录并支付1元继续阅读"}</p>
<div className="space-y-3 mb-4">
<button
onClick={() => handlePurchaseClick("section")}
className="w-full py-3 px-5 rounded-xl bg-[#ff3b5c] text-white font-medium active:scale-[0.98] transition-transform"
>
<div className="flex items-center justify-center gap-2">
<span></span>
<span className="text-lg font-bold">¥1</span>
</div>
</button>
<button
onClick={() => handlePurchaseClick("fullbook")}
className="w-full py-3 px-5 rounded-xl bg-[#2c2c2e] border border-white/10 text-white font-medium active:scale-[0.98] transition-transform"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-[#ffd700]" />
<span> {totalSections} </span>
</div>
<span className="text-[#ffd700]">¥{fullBookPrice}</span>
</div>
</button>
</div>
<p className="text-xs text-gray-500">90%</p>
</div>
</div>
</div>
)}
{/* 底部引导 - 免费章节或已购买时显示 */}
{!showPaywall && (
<div className="mt-8 p-4 rounded-xl bg-[#1c1c1e] border border-white/5">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium"></p>
<p className="text-gray-500 text-sm mt-1"> {totalSections} </p>
</div>
<button
onClick={() => router.push("/chapters")}
className="px-4 py-2 rounded-lg bg-[#ff3b5c] text-white text-sm font-medium flex items-center gap-1"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
)}
</main>
{/* 底部导航 */}
<nav className="fixed bottom-0 left-0 right-0 bg-[#1c1c1e]/95 backdrop-blur-xl border-t border-white/5 pb-safe-bottom">
<div className="px-4 py-3">
<div className="flex items-center justify-between gap-2">
<button
onClick={() => router.push("/chapters")}
className="flex-1 py-2.5 rounded-xl bg-[#2c2c2e] text-white text-sm font-medium text-center active:bg-[#3c3c3e]"
>
</button>
<button
onClick={() => router.push("/about")}
className="flex-1 py-2.5 rounded-xl bg-[#2c2c2e] text-white text-sm font-medium text-center active:bg-[#3c3c3e]"
>
</button>
<button
onClick={() => router.push("/my")}
className="flex-1 py-2.5 rounded-xl bg-[#2c2c2e] text-white text-sm font-medium text-center active:bg-[#3c3c3e]"
>
</button>
<button
onClick={() => handlePurchaseClick("fullbook")}
className="flex-1 py-2.5 rounded-xl bg-[#ff3b5c] text-white text-sm font-medium text-center active:scale-[0.98]"
>
</button>
</div>
</div>
</nav>
{/* 登录弹窗 */}
<AuthModal isOpen={isAuthOpen} onClose={() => setIsAuthOpen(false)} />
{/* 支付弹窗 */}
<PaymentModal
isOpen={isPaymentOpen}
onClose={() => setIsPaymentOpen(false)}
type={paymentType}
sectionId={latestSection.id}
sectionTitle={latestSection.title}
amount={paymentType === "section" ? latestSection.price : fullBookPrice}
onSuccess={() => window.location.reload()}
/>
</div>
)
}