Files
soul/components/reading-modal.tsx
v0 59ca3b2bbd refactor: full product interaction system redesign
Refactor homepage, reading modal, matching feature, and user profile for improved UX

#VERCEL_SKIP

Co-authored-by: undefined <undefined+undefined@users.noreply.github.com>
2026-01-14 05:17:59 +00:00

126 lines
4.9 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 } from "react"
import { X, Lock, Sparkles } from "lucide-react"
import { useStore } from "@/lib/store"
import { getFullBookPrice } from "@/lib/book-data"
interface ReadingModalProps {
section: { id: string; title: string; filePath: string }
onClose: () => void
onPurchase: (sectionId: string, title: string, price: number) => void
}
export function ReadingModal({ section, onClose, onPurchase }: ReadingModalProps) {
const [content, setContent] = useState("")
const [isLoading, setIsLoading] = useState(true)
const { isLoggedIn, hasPurchased } = useStore()
const isFree = section.id === "preface" || section.id === "epilogue"
const canAccess = isFree || (isLoggedIn && hasPurchased(section.id))
const fullBookPrice = getFullBookPrice()
useEffect(() => {
async function loadContent() {
try {
const response = await fetch(`/api/content?path=${encodeURIComponent(section.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()
}, [section.filePath])
// 计算显示内容
const displayContent = canAccess ? content : content.slice(0, Math.floor(content.length * 0.3))
const showPaywall = !canAccess && content.length > 0
return (
<div className="fixed inset-0 z-50 bg-black/90 modal-overlay">
<div className="h-full flex flex-col">
{/* Header */}
<header className="flex-shrink-0 glass-nav px-4 py-3 flex items-center justify-between safe-top">
<button
onClick={onClose}
className="w-9 h-9 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center touch-feedback"
>
<X className="w-5 h-5" />
</button>
<h1 className="text-white font-semibold text-sm truncate flex-1 mx-4 text-center">{section.title}</h1>
<div className="w-9" />
</header>
{/* Content */}
<div className="flex-1 overflow-y-auto scrollbar-hide">
<div className="max-w-2xl mx-auto px-5 py-6">
{isLoading ? (
<div className="space-y-4">
{[...Array(10)].map((_, i) => (
<div key={i} className="skeleton h-4 rounded" style={{ width: `${Math.random() * 40 + 60}%` }} />
))}
</div>
) : (
<>
<article className="book-content relative">
<div className="text-[var(--app-text-secondary)] leading-[1.9] text-[17px]">
{displayContent.split("\n").map(
(paragraph, index) =>
paragraph.trim() && (
<p key={index} className="mb-6 text-justify">
{paragraph}
</p>
),
)}
</div>
{/* 付费墙渐变 */}
{showPaywall && (
<div className="absolute bottom-0 left-0 right-0 h-48 bg-gradient-to-t from-black to-transparent" />
)}
</article>
{/* 付费提示 - 在阅读中途触发 */}
{showPaywall && (
<div className="mt-8 glass-card-heavy p-8 text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-[var(--app-bg-secondary)] flex items-center justify-center">
<Lock className="w-8 h-8 text-[var(--app-text-tertiary)]" />
</div>
<h3 className="text-xl font-semibold mb-2"></h3>
<p className="text-[var(--app-text-secondary)] mb-6">30%</p>
<div className="flex flex-col gap-3">
<button
onClick={() => onPurchase(section.id, section.title, 1)}
className="btn-ios-secondary py-3"
>
¥1
</button>
<button
onClick={() => onPurchase(section.id, section.title, fullBookPrice)}
className="btn-ios py-3 glow flex items-center justify-center gap-2"
>
<Sparkles className="w-4 h-4" />
¥{fullBookPrice} 82%
</button>
</div>
<p className="text-[var(--app-text-tertiary)] text-xs mt-4">
90%
</p>
</div>
)}
</>
)}
</div>
</div>
</div>
</div>
)
}