Files
soul/app/page.tsx

275 lines
11 KiB
TypeScript
Raw Normal View History

"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>
)
}