chore: 恢复上传 components 目录到 GitHub
This commit is contained in:
290
components/chapter-content.tsx
Normal file
290
components/chapter-content.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { ChevronLeft, Lock, Share2, BookOpen, Clock, MessageCircle } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { type Section, getFullBookPrice, isSectionUnlocked } from "@/lib/book-data"
|
||||
import { useStore } from "@/lib/store"
|
||||
import { AuthModal } from "./modules/auth/auth-modal"
|
||||
import { PaymentModal } from "./modules/payment/payment-modal"
|
||||
import { UserMenu } from "./user-menu"
|
||||
import { QRCodeModal } from "./modules/marketing/qr-code-modal"
|
||||
import { ReferralShare } from "./modules/referral/referral-share"
|
||||
|
||||
interface ChapterContentProps {
|
||||
section: Section & { filePath: string }
|
||||
partTitle: string
|
||||
chapterTitle: string
|
||||
}
|
||||
|
||||
export function ChapterContent({ section, partTitle, chapterTitle }: ChapterContentProps) {
|
||||
const [content, setContent] = useState<string>("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isAuthOpen, setIsAuthOpen] = useState(false)
|
||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false)
|
||||
const [isQRModalOpen, setIsQRModalOpen] = useState(false)
|
||||
const [paymentType, setPaymentType] = useState<"section" | "fullbook">("section")
|
||||
const [fullBookPrice, setFullBookPrice] = useState(9.9)
|
||||
|
||||
const { user, isLoggedIn, hasPurchased, settings } = useStore()
|
||||
const distributorShare = settings?.distributorShare || 90
|
||||
|
||||
const isUnlocked = isSectionUnlocked(section)
|
||||
const canAccess = section.isFree || isUnlocked || (isLoggedIn && hasPurchased(section.id))
|
||||
|
||||
useEffect(() => {
|
||||
setFullBookPrice(getFullBookPrice())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
async function loadContent() {
|
||||
try {
|
||||
if (section.content) {
|
||||
setContent(section.content)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && section.filePath.startsWith("custom/")) {
|
||||
const customSections = JSON.parse(localStorage.getItem("custom_sections") || "[]") as Section[]
|
||||
const customSection = customSections.find((s) => s.id === section.id)
|
||||
if (customSection?.content) {
|
||||
setContent(customSection.content)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/content?path=${encodeURIComponent(section.filePath)}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (!data.isCustom) {
|
||||
setContent(data.content)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load content:", error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadContent()
|
||||
}, [section.filePath, section.id, section.content])
|
||||
|
||||
const handlePurchaseClick = (type: "section" | "fullbook") => {
|
||||
if (!isLoggedIn) {
|
||||
setIsAuthOpen(true)
|
||||
return
|
||||
}
|
||||
setPaymentType(type)
|
||||
setIsPaymentOpen(true)
|
||||
}
|
||||
|
||||
const handleShare = async () => {
|
||||
const url = user?.referralCode ? `${window.location.href}?ref=${user.referralCode}` : window.location.href
|
||||
const shareData = {
|
||||
title: section.title,
|
||||
text: `来自Soul派对房的真实商业故事: ${section.title}`,
|
||||
url: url,
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.share && navigator.canShare && navigator.canShare(shareData)) {
|
||||
await navigator.share(shareData)
|
||||
} else {
|
||||
navigator.clipboard.writeText(url)
|
||||
alert(
|
||||
`链接已复制!分享后他人购买,你可获得${distributorShare}%返利 (¥${((fullBookPrice * distributorShare) / 100).toFixed(1)})`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as Error).name !== "AbortError") {
|
||||
navigator.clipboard.writeText(url)
|
||||
alert(`链接已复制!分享后他人购买,你可获得${distributorShare}%返利`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previewContent = content.slice(0, 500)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a1628] text-white pb-20">
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-50 bg-[#0a1628]/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-3xl mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<Link href="/chapters" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">目录</span>
|
||||
</Link>
|
||||
<div className="text-center flex-1 px-4">
|
||||
<p className="text-gray-500 text-xs">{partTitle}</p>
|
||||
{chapterTitle && <p className="text-gray-400 text-sm truncate">{chapterTitle}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ReferralShare
|
||||
sectionTitle={section.title}
|
||||
fullBookPrice={fullBookPrice}
|
||||
distributorShare={distributorShare}
|
||||
/>
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<main className="max-w-3xl mx-auto px-4 py-8">
|
||||
{/* Title */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-2 text-[#38bdac] text-sm mb-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span>{section.id}</span>
|
||||
{section.unlockAfterDays && !section.isFree && (
|
||||
<span className="ml-2 px-2 py-0.5 bg-orange-500/20 text-orange-400 text-xs rounded-full flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{isUnlocked ? "已免费解锁" : `${section.unlockAfterDays}天后免费`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-white leading-tight">{section.title}</h1>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-[#38bdac] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : canAccess ? (
|
||||
<>
|
||||
<article className="prose prose-invert prose-lg max-w-none">
|
||||
<div className="text-gray-300 leading-relaxed whitespace-pre-line">
|
||||
{content.split("\n").map((paragraph, index) => (
|
||||
<p key={index} className="mb-4">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{/* Join Party Group CTA */}
|
||||
<div className="mt-12 bg-gradient-to-r from-[#38bdac]/10 to-[#1a3a4a]/50 rounded-2xl p-6 border border-[#38bdac]/30">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#38bdac]/20 flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="w-6 h-6 text-[#38bdac]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-semibold mb-1">想听更多商业故事?</h3>
|
||||
<p className="text-gray-400 text-sm">每天早上6-9点,卡若在Soul派对房分享真实案例</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsQRModalOpen(true)}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white whitespace-nowrap"
|
||||
>
|
||||
加入派对群
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<article className="prose prose-invert prose-lg max-w-none relative">
|
||||
<div className="text-gray-300 leading-relaxed whitespace-pre-line">
|
||||
{previewContent.split("\n").map((paragraph, index) => (
|
||||
<p key={index} className="mb-4">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute bottom-0 left-0 right-0 h-40 bg-gradient-to-t from-[#0a1628] to-transparent" />
|
||||
</article>
|
||||
|
||||
{/* Purchase prompt */}
|
||||
<div className="mt-8 bg-[#0f2137]/80 backdrop-blur-xl rounded-2xl p-8 border border-gray-700/50 text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-800/50 flex items-center justify-center">
|
||||
<Lock className="w-8 h-8 text-gray-500" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white mb-2">解锁完整内容</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
{isLoggedIn ? "购买本节或整本书以阅读完整内容" : "登录后购买即可阅读完整内容"}
|
||||
</p>
|
||||
|
||||
{section.unlockAfterDays && (
|
||||
<p className="text-orange-400 text-sm mb-4 flex items-center justify-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
本节将在{section.unlockAfterDays}天后免费解锁
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
onClick={() => handlePurchaseClick("section")}
|
||||
variant="outline"
|
||||
className="border-gray-600 text-white hover:bg-gray-700/50 px-6 py-5"
|
||||
>
|
||||
购买本节 ¥{section.price}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handlePurchaseClick("fullbook")}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white px-6 py-5"
|
||||
>
|
||||
购买全书 ¥{fullBookPrice.toFixed(1)}
|
||||
<span className="ml-2 text-xs opacity-80">省82%</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500 text-sm mt-4">
|
||||
分享本书,他人购买你可获得 <span className="text-[#38bdac]">{distributorShare}%返利</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Join Party Group */}
|
||||
<div className="mt-8 bg-gradient-to-r from-[#38bdac]/10 to-[#1a3a4a]/50 rounded-2xl p-6 border border-[#38bdac]/30">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#38bdac]/20 flex items-center justify-center flex-shrink-0">
|
||||
<MessageCircle className="w-6 h-6 text-[#38bdac]" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-white font-semibold mb-1">不想花钱?来派对群免费听!</h3>
|
||||
<p className="text-gray-400 text-sm">每天早上6-9点,卡若在Soul派对房免费分享</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setIsQRModalOpen(true)}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white whitespace-nowrap"
|
||||
>
|
||||
加入派对群
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="mt-12 pt-8 border-t border-gray-800 flex justify-between">
|
||||
<Link href="/chapters" className="text-gray-400 hover:text-white transition-colors">
|
||||
← 返回目录
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="text-[#38bdac] hover:text-[#4fd4c4] transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
分享赚 ¥{((section.price * distributorShare) / 100).toFixed(1)}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Modals */}
|
||||
<AuthModal isOpen={isAuthOpen} onClose={() => setIsAuthOpen(false)} />
|
||||
<PaymentModal
|
||||
isOpen={isPaymentOpen}
|
||||
onClose={() => setIsPaymentOpen(false)}
|
||||
type={paymentType}
|
||||
sectionId={section.id}
|
||||
sectionTitle={section.title}
|
||||
amount={paymentType === "section" ? section.price : fullBookPrice}
|
||||
onSuccess={() => window.location.reload()}
|
||||
/>
|
||||
<QRCodeModal isOpen={isQRModalOpen} onClose={() => setIsQRModalOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user