diff --git a/.gitignore b/.gitignore
index e18b410..2d75c7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
node_modules/
-components/
.next/
.env.local
.DS_Store
diff --git a/components/auth-modal.tsx b/components/auth-modal.tsx
new file mode 100644
index 0000000..a4b1a91
--- /dev/null
+++ b/components/auth-modal.tsx
@@ -0,0 +1,225 @@
+"use client"
+
+import { useState } from "react"
+import { X, Phone, User, Gift } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { useStore } from "@/lib/store"
+
+interface AuthModalProps {
+ isOpen: boolean
+ onClose: () => void
+ defaultTab?: "login" | "register"
+}
+
+export function AuthModal({ isOpen, onClose, defaultTab = "login" }: AuthModalProps) {
+ const [tab, setTab] = useState<"login" | "register">(defaultTab)
+ const [phone, setPhone] = useState("")
+ const [code, setCode] = useState("")
+ const [nickname, setNickname] = useState("")
+ const [referralCode, setReferralCode] = useState("")
+ const [error, setError] = useState("")
+ const [codeSent, setCodeSent] = useState(false)
+
+ const { login, register } = useStore()
+
+ const handleSendCode = () => {
+ if (phone.length !== 11) {
+ setError("请输入正确的手机号")
+ return
+ }
+ // Simulate sending verification code
+ setCodeSent(true)
+ setError("")
+ alert("验证码已发送,测试验证码: 123456")
+ }
+
+ const handleLogin = async () => {
+ setError("")
+ const success = await login(phone, code)
+ if (success) {
+ onClose()
+ } else {
+ setError("验证码错误或用户不存在,请先注册")
+ }
+ }
+
+ const handleRegister = async () => {
+ setError("")
+ if (!nickname.trim()) {
+ setError("请输入昵称")
+ return
+ }
+ const success = await register(phone, nickname, referralCode || undefined)
+ if (success) {
+ onClose()
+ } else {
+ setError("该手机号已注册")
+ }
+ }
+
+ if (!isOpen) return null
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Close button */}
+
+
+ {/* Tabs */}
+
+
+
+
+
+ {/* Content */}
+
+ {tab === "login" ? (
+
+
+
+
+
+
setPhone(e.target.value)}
+ placeholder="请输入手机号"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={11}
+ />
+
+
+
+
+
+
+ setCode(e.target.value)}
+ placeholder="请输入验证码"
+ className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={6}
+ />
+
+
+
+
+ {error &&
{error}
}
+
+
+
+ ) : (
+
+
+
+
+
+
setPhone(e.target.value)}
+ placeholder="请输入手机号"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={11}
+ />
+
+
+
+
+
+
+
+ setNickname(e.target.value)}
+ placeholder="请输入昵称"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ />
+
+
+
+
+
+
+ setCode(e.target.value)}
+ placeholder="请输入验证码"
+ className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={6}
+ />
+
+
+
+
+
+
+
+
+ setReferralCode(e.target.value)}
+ placeholder="填写邀请码可获得优惠"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ />
+
+
+
+ {error &&
{error}
}
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/components/book-cover.tsx b/components/book-cover.tsx
new file mode 100644
index 0000000..4477e3f
--- /dev/null
+++ b/components/book-cover.tsx
@@ -0,0 +1,111 @@
+"use client"
+import { useState, useEffect } from "react"
+import { Button } from "@/components/ui/button"
+import { BookOpen } from "lucide-react"
+import Link from "next/link"
+import { getFullBookPrice, getAllSections } from "@/lib/book-data"
+import { useStore } from "@/lib/store"
+import { AuthModal } from "./modules/auth/auth-modal"
+import { PaymentModal } from "./modules/payment/payment-modal"
+
+export function BookCover() {
+ const [fullBookPrice, setFullBookPrice] = useState(9.9)
+ const [sectionsCount, setSectionsCount] = useState(55)
+ const [isAuthOpen, setIsAuthOpen] = useState(false)
+ const [isPaymentOpen, setIsPaymentOpen] = useState(false)
+ const { isLoggedIn } = useStore()
+
+ useEffect(() => {
+ const sections = getAllSections()
+ setSectionsCount(sections.length)
+ setFullBookPrice(getFullBookPrice(sections.length))
+ }, [])
+
+ return (
+
+ {/* Background decorative lines - simplified */}
+
+
+ {/* Content - more compact for mobile */}
+
+ {/* Soul badge */}
+
+ Soul · 派对房
+
+
+ {/* Main title - smaller on mobile */}
+
+ 一场SOUL的
+
+ 创业实验场
+
+
+ {/* Subtitle */}
+
来自Soul派对房的真实商业故事
+
+ {/* Quote - smaller */}
+
"社会不是靠努力,是靠洞察与选择"
+
+ {/* Price info - compact card */}
+
+
+
+
¥{fullBookPrice.toFixed(1)}
+
整本价格
+
+
+
+
{sectionsCount}
+
商业案例
+
+
+
+
+ {/* Author info - compact */}
+
+
+ {/* CTA Button */}
+
+
+
+
+
首章免费 · 部分章节3天后解锁
+
+
+ {/* Modals */}
+ setIsAuthOpen(false)} />
+ setIsPaymentOpen(false)}
+ type="fullbook"
+ amount={fullBookPrice}
+ onSuccess={() => window.location.reload()}
+ />
+
+ )
+}
diff --git a/components/book-intro.tsx b/components/book-intro.tsx
new file mode 100644
index 0000000..e008de8
--- /dev/null
+++ b/components/book-intro.tsx
@@ -0,0 +1,34 @@
+export function BookIntro() {
+ return (
+
+
+ {/* Glass card */}
+
+ {/* Quote */}
+
+ "这不是一本教你成功的鸡汤书。这是我每天早上6点到9点,在Soul派对房和几百个陌生人分享的真实故事。"
+
+
+ {/* Author */}
+
— 卡若
+
+ {/* Stats */}
+
+
+
+
+ )
+}
diff --git a/components/bottom-nav.tsx b/components/bottom-nav.tsx
new file mode 100644
index 0000000..e12d8fd
--- /dev/null
+++ b/components/bottom-nav.tsx
@@ -0,0 +1,62 @@
+"use client"
+
+import Link from "next/link"
+import { usePathname } from "next/navigation"
+import { Home, MessageCircle, User } from "lucide-react"
+import { useState } from "react"
+import { QRCodeModal } from "./modules/marketing/qr-code-modal"
+
+export function BottomNav() {
+ const pathname = usePathname()
+ const [showQRModal, setShowQRModal] = useState(false)
+
+ if (pathname.startsWith("/documentation")) {
+ return null
+ }
+
+ const navItems = [
+ { href: "/", icon: Home, label: "首页" },
+ { action: () => setShowQRModal(true), icon: MessageCircle, label: "派对群" },
+ { href: "/my", icon: User, label: "我的" },
+ ]
+
+ return (
+ <>
+
+ setShowQRModal(false)} />
+ >
+ )
+}
diff --git a/components/buy-full-book-button.tsx b/components/buy-full-book-button.tsx
new file mode 100644
index 0000000..16a1a8f
--- /dev/null
+++ b/components/buy-full-book-button.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/lib/store"
+import { PaymentModal } from "@/components/modules/payment/payment-modal"
+import { AuthModal } from "@/components/modules/auth/auth-modal"
+
+interface BuyFullBookButtonProps {
+ price: number
+ className?: string
+ size?: "default" | "sm" | "lg" | "icon"
+ children?: React.ReactNode
+}
+
+export function BuyFullBookButton({ price, className, size = "default", children }: BuyFullBookButtonProps) {
+ const [isPaymentOpen, setIsPaymentOpen] = useState(false)
+ const [isAuthOpen, setIsAuthOpen] = useState(false)
+ const { isLoggedIn } = useStore()
+
+ const handleClick = () => {
+ if (!isLoggedIn) {
+ setIsAuthOpen(true)
+ return
+ }
+ setIsPaymentOpen(true)
+ }
+
+ return (
+ <>
+
+
+ setIsAuthOpen(false)}
+ />
+
+ setIsPaymentOpen(false)}
+ type="fullbook"
+ amount={price}
+ onSuccess={() => {
+ // Refresh or redirect
+ window.location.reload()
+ }}
+ />
+ >
+ )
+}
diff --git a/components/chapter-content.tsx b/components/chapter-content.tsx
new file mode 100644
index 0000000..13584b0
--- /dev/null
+++ b/components/chapter-content.tsx
@@ -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("")
+ 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 (
+
+ {/* Header */}
+
+
+
+
+
目录
+
+
+
{partTitle}
+ {chapterTitle &&
{chapterTitle}
}
+
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {/* Title */}
+
+
+
+ {section.id}
+ {section.unlockAfterDays && !section.isFree && (
+
+
+ {isUnlocked ? "已免费解锁" : `${section.unlockAfterDays}天后免费`}
+
+ )}
+
+
{section.title}
+
+
+ {isLoading ? (
+
+ ) : canAccess ? (
+ <>
+
+
+ {content.split("\n").map((paragraph, index) => (
+
+ {paragraph}
+
+ ))}
+
+
+
+ {/* Join Party Group CTA */}
+
+
+
+
+
+
+
想听更多商业故事?
+
每天早上6-9点,卡若在Soul派对房分享真实案例
+
+
+
+
+ >
+ ) : (
+
+
+
+ {previewContent.split("\n").map((paragraph, index) => (
+
+ {paragraph}
+
+ ))}
+
+
+
+
+ {/* Purchase prompt */}
+
+
+
+
+
解锁完整内容
+
+ {isLoggedIn ? "购买本节或整本书以阅读完整内容" : "登录后购买即可阅读完整内容"}
+
+
+ {section.unlockAfterDays && (
+
+
+ 本节将在{section.unlockAfterDays}天后免费解锁
+
+ )}
+
+
+
+
+
+
+
+ 分享本书,他人购买你可获得 {distributorShare}%返利
+
+
+
+ {/* Join Party Group */}
+
+
+
+
+
+
+
不想花钱?来派对群免费听!
+
每天早上6-9点,卡若在Soul派对房免费分享
+
+
+
+
+
+ )}
+
+ {/* Navigation */}
+
+
+ ← 返回目录
+
+
+
+
+
+ {/* Modals */}
+
setIsAuthOpen(false)} />
+ setIsPaymentOpen(false)}
+ type={paymentType}
+ sectionId={section.id}
+ sectionTitle={section.title}
+ amount={paymentType === "section" ? section.price : fullBookPrice}
+ onSuccess={() => window.location.reload()}
+ />
+ setIsQRModalOpen(false)} />
+
+ )
+}
diff --git a/components/chapters-list.tsx b/components/chapters-list.tsx
new file mode 100644
index 0000000..30ca867
--- /dev/null
+++ b/components/chapters-list.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import { useState } from "react"
+import Link from "next/link"
+import { ChevronRight, Lock, Unlock, BookOpen } from "lucide-react"
+import { Part } from "@/lib/book-data"
+
+interface ChaptersListProps {
+ parts: Part[]
+ specialSections?: {
+ preface?: { title: string }
+ epilogue?: { title: string }
+ }
+}
+
+export function ChaptersList({ parts, specialSections }: ChaptersListProps) {
+ const [expandedPart, setExpandedPart] = useState(parts.length > 0 ? parts[0].id : null)
+
+ return (
+
+ {/* Special sections - Preface */}
+ {specialSections?.preface && (
+
+
+
+
+
+
+ {specialSections.preface.title}
+
+
+
免费
+
+
+
+ )}
+
+ {/* Parts */}
+
+ {parts.map((part) => (
+
+ {/* Part header */}
+
+
+ {/* Chapters and sections */}
+ {expandedPart === part.id && (
+
+ {part.chapters.map((chapter) => (
+
+ {/* Chapter title */}
+
+
+
+ {chapter.title}
+
+
+
+ {/* Sections */}
+
+ {chapter.sections.map((section) => (
+
+
+
+ {section.isFree ? (
+
+ ) : (
+
+ )}
+ {section.id}
+
+ {section.title}
+
+
+
+ {section.isFree ? (
+ 免费
+ ) : (
+ ¥{section.price}
+ )}
+
+
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ {/* Special sections - Epilogue */}
+ {specialSections?.epilogue && (
+
+
+
+
+
+
+ {specialSections.epilogue.title}
+
+
+
免费
+
+
+
+ )}
+
+ )
+}
diff --git a/components/config-loader.tsx b/components/config-loader.tsx
new file mode 100644
index 0000000..5a6221b
--- /dev/null
+++ b/components/config-loader.tsx
@@ -0,0 +1,14 @@
+"use client"
+
+import { useEffect } from "react"
+import { useStore } from "@/lib/store"
+
+export function ConfigLoader() {
+ const { fetchSettings } = useStore()
+
+ useEffect(() => {
+ fetchSettings()
+ }, [fetchSettings])
+
+ return null
+}
diff --git a/components/footer.tsx b/components/footer.tsx
new file mode 100644
index 0000000..2cd0a8e
--- /dev/null
+++ b/components/footer.tsx
@@ -0,0 +1,12 @@
+"use client"
+
+export function Footer() {
+ return (
+
+ )
+}
diff --git a/components/layout-wrapper.tsx b/components/layout-wrapper.tsx
new file mode 100644
index 0000000..a9c6343
--- /dev/null
+++ b/components/layout-wrapper.tsx
@@ -0,0 +1,27 @@
+"use client"
+
+import { usePathname } from "next/navigation"
+import { BottomNav } from "@/components/bottom-nav"
+import { ConfigLoader } from "@/components/config-loader"
+
+export function LayoutWrapper({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname()
+ const isAdmin = pathname?.startsWith("/admin")
+
+ if (isAdmin) {
+ return (
+
+
+ {children}
+
+ )
+ }
+
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/components/modules/auth/auth-modal.tsx b/components/modules/auth/auth-modal.tsx
new file mode 100644
index 0000000..a4b1a91
--- /dev/null
+++ b/components/modules/auth/auth-modal.tsx
@@ -0,0 +1,225 @@
+"use client"
+
+import { useState } from "react"
+import { X, Phone, User, Gift } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { useStore } from "@/lib/store"
+
+interface AuthModalProps {
+ isOpen: boolean
+ onClose: () => void
+ defaultTab?: "login" | "register"
+}
+
+export function AuthModal({ isOpen, onClose, defaultTab = "login" }: AuthModalProps) {
+ const [tab, setTab] = useState<"login" | "register">(defaultTab)
+ const [phone, setPhone] = useState("")
+ const [code, setCode] = useState("")
+ const [nickname, setNickname] = useState("")
+ const [referralCode, setReferralCode] = useState("")
+ const [error, setError] = useState("")
+ const [codeSent, setCodeSent] = useState(false)
+
+ const { login, register } = useStore()
+
+ const handleSendCode = () => {
+ if (phone.length !== 11) {
+ setError("请输入正确的手机号")
+ return
+ }
+ // Simulate sending verification code
+ setCodeSent(true)
+ setError("")
+ alert("验证码已发送,测试验证码: 123456")
+ }
+
+ const handleLogin = async () => {
+ setError("")
+ const success = await login(phone, code)
+ if (success) {
+ onClose()
+ } else {
+ setError("验证码错误或用户不存在,请先注册")
+ }
+ }
+
+ const handleRegister = async () => {
+ setError("")
+ if (!nickname.trim()) {
+ setError("请输入昵称")
+ return
+ }
+ const success = await register(phone, nickname, referralCode || undefined)
+ if (success) {
+ onClose()
+ } else {
+ setError("该手机号已注册")
+ }
+ }
+
+ if (!isOpen) return null
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Close button */}
+
+
+ {/* Tabs */}
+
+
+
+
+
+ {/* Content */}
+
+ {tab === "login" ? (
+
+
+
+
+
+
setPhone(e.target.value)}
+ placeholder="请输入手机号"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={11}
+ />
+
+
+
+
+
+
+ setCode(e.target.value)}
+ placeholder="请输入验证码"
+ className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={6}
+ />
+
+
+
+
+ {error &&
{error}
}
+
+
+
+ ) : (
+
+
+
+
+
+
setPhone(e.target.value)}
+ placeholder="请输入手机号"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={11}
+ />
+
+
+
+
+
+
+
+ setNickname(e.target.value)}
+ placeholder="请输入昵称"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ />
+
+
+
+
+
+
+ setCode(e.target.value)}
+ placeholder="请输入验证码"
+ className="bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ maxLength={6}
+ />
+
+
+
+
+
+
+
+
+ setReferralCode(e.target.value)}
+ placeholder="填写邀请码可获得优惠"
+ className="pl-10 bg-[#0a1628] border-gray-700 text-white placeholder:text-gray-500"
+ />
+
+
+
+ {error &&
{error}
}
+
+
+
+ )}
+
+
+
+ )
+}
diff --git a/components/modules/marketing/qr-code-modal.tsx b/components/modules/marketing/qr-code-modal.tsx
new file mode 100644
index 0000000..878878e
--- /dev/null
+++ b/components/modules/marketing/qr-code-modal.tsx
@@ -0,0 +1,123 @@
+"use client"
+
+import { X, MessageCircle, Users, Music } from "lucide-react"
+import { useStore } from "@/lib/store"
+import { Button } from "@/components/ui/button"
+import Image from "next/image"
+import { useState, useEffect } from "react"
+
+interface QRCodeModalProps {
+ isOpen: boolean
+ onClose: () => void
+}
+
+export function QRCodeModal({ isOpen, onClose }: QRCodeModalProps) {
+ const { settings, getLiveQRCodeUrl } = useStore()
+ const [isJoining, setIsJoining] = useState(false)
+ const [qrCodeUrl, setQrCodeUrl] = useState("/images/party-group-qr.png") // Default fallback
+
+ // Fetch config on mount
+ useEffect(() => {
+ const fetchConfig = async () => {
+ try {
+ const res = await fetch('/api/config')
+ const data = await res.json()
+ if (data.marketing?.partyGroup?.qrCode) {
+ setQrCodeUrl(data.marketing.partyGroup.qrCode)
+ }
+ } catch (e) {
+ console.error("Failed to load QR config", e)
+ }
+ }
+ fetchConfig()
+ }, [])
+
+ if (!isOpen) return null
+
+ const handleJoin = () => {
+ setIsJoining(true)
+ // 获取活码随机URL
+ const url = getLiveQRCodeUrl("party-group")
+ if (url) {
+ window.open(url, "_blank")
+ }
+ setTimeout(() => setIsJoining(false), 1000)
+ }
+
+ return (
+
+
+
+ {/* iOS Style Modal */}
+
+
+ {/* Header Background Effect */}
+
+
+
+
+
+
+ {/* Icon Badge */}
+
+
+
+ Soul 创业派对
+
+
+
+
+ Live
+
+
+ {settings.authorInfo?.liveTime || "06:00-09:00"} · {settings.authorInfo?.name || "卡若"}
+
+
+
+ {/* QR Code Container - Enhanced Visibility */}
+
+
+
+ 扫码加入私域流量实战群
获取《私域运营100问》
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/modules/payment/payment-modal.tsx b/components/modules/payment/payment-modal.tsx
new file mode 100644
index 0000000..7d4cff2
--- /dev/null
+++ b/components/modules/payment/payment-modal.tsx
@@ -0,0 +1,343 @@
+"use client"
+
+import type React from "react"
+import { useState } from "react"
+import { X, CheckCircle, Bitcoin, Globe, Copy, Check } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/lib/store"
+
+const WechatIcon = () => (
+
+)
+
+const AlipayIcon = () => (
+
+)
+
+type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal"
+
+interface PaymentModalProps {
+ isOpen: boolean
+ onClose: () => void
+ type: "section" | "fullbook"
+ sectionId?: string
+ sectionTitle?: string
+ amount: number
+ onSuccess: () => void
+}
+
+export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, amount, onSuccess }: PaymentModalProps) {
+ const [paymentMethod, setPaymentMethod] = useState("wechat")
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const [showPaymentDetails, setShowPaymentDetails] = useState(false)
+ const [copied, setCopied] = useState(false)
+
+ const { purchaseSection, purchaseFullBook, user, settings } = useStore()
+
+ const paymentConfig = settings?.paymentMethods || {
+ wechat: { enabled: true, qrCode: "", account: "" },
+ alipay: { enabled: true, qrCode: "", account: "" },
+ usdt: { enabled: true, network: "TRC20", address: "", exchangeRate: 7.2 },
+ paypal: { enabled: false, email: "", exchangeRate: 7.2 },
+ }
+
+ const usdtAmount = (amount / (paymentConfig.usdt.exchangeRate || 7.2)).toFixed(2)
+ const paypalAmount = (amount / (paymentConfig.paypal.exchangeRate || 7.2)).toFixed(2)
+
+ const handleCopyAddress = (address: string) => {
+ navigator.clipboard.writeText(address)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const handlePayment = async () => {
+ if (paymentMethod === "usdt" || paymentMethod === "paypal" || paymentMethod === "wechat" || paymentMethod === "alipay") {
+ setShowPaymentDetails(true)
+ return
+ }
+
+ setIsProcessing(true)
+ await new Promise((resolve) => setTimeout(resolve, 1500))
+
+ let success = false
+ if (type === "section" && sectionId) {
+ success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
+ } else if (type === "fullbook") {
+ success = await purchaseFullBook(paymentMethod)
+ }
+
+ setIsProcessing(false)
+
+ if (success) {
+ setIsSuccess(true)
+ setTimeout(() => {
+ onSuccess()
+ onClose()
+ setIsSuccess(false)
+ }, 1500)
+ }
+ }
+
+ const confirmCryptoPayment = async () => {
+ setIsProcessing(true)
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ let success = false
+ if (type === "section" && sectionId) {
+ success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
+ } else if (type === "fullbook") {
+ success = await purchaseFullBook(paymentMethod)
+ }
+
+ setIsProcessing(false)
+ setShowPaymentDetails(false)
+
+ if (success) {
+ setIsSuccess(true)
+ setTimeout(() => {
+ onSuccess()
+ onClose()
+ setIsSuccess(false)
+ }, 1500)
+ }
+ }
+
+ if (!isOpen) return null
+
+ const paymentMethods: {
+ id: PaymentMethod
+ name: string
+ icon: React.ReactNode
+ color: string
+ enabled: boolean
+ extra?: string
+ }[] = [
+ {
+ id: "wechat",
+ name: "微信支付",
+ icon: ,
+ color: "bg-[#07C160]",
+ enabled: paymentConfig.wechat.enabled,
+ },
+ {
+ id: "alipay",
+ name: "支付宝",
+ icon: ,
+ color: "bg-[#1677FF]",
+ enabled: paymentConfig.alipay.enabled,
+ },
+ {
+ id: "usdt",
+ name: `USDT (${paymentConfig.usdt.network || "TRC20"})`,
+ icon: ,
+ color: "bg-[#26A17B]",
+ enabled: paymentConfig.usdt.enabled,
+ extra: `≈ $${usdtAmount}`,
+ },
+ {
+ id: "paypal",
+ name: "PayPal",
+ icon: ,
+ color: "bg-[#003087]",
+ enabled: paymentConfig.paypal.enabled,
+ extra: `≈ $${paypalAmount}`,
+ },
+ ]
+
+ const availableMethods = paymentMethods.filter((m) => m.enabled)
+
+ // Payment details view
+ if (showPaymentDetails) {
+ const isCrypto = paymentMethod === "usdt"
+ const isPayPal = paymentMethod === "paypal"
+ const isWechat = paymentMethod === "wechat"
+ const isAlipay = paymentMethod === "alipay"
+
+ let title = ""
+ let address = ""
+ let displayAmount = `¥${amount.toFixed(2)}`
+ let qrCodeUrl = ""
+
+ if (isCrypto) {
+ title = "USDT支付"
+ address = paymentConfig.usdt.address
+ displayAmount = `$${usdtAmount} USDT`
+ } else if (isPayPal) {
+ title = "PayPal支付"
+ address = paymentConfig.paypal.email
+ displayAmount = `$${paypalAmount} USD`
+ } else if (isWechat) {
+ title = "微信支付"
+ qrCodeUrl = paymentConfig.wechat.qrCode || "/images/wechat-pay.png"
+ } else if (isAlipay) {
+ title = "支付宝支付"
+ qrCodeUrl = paymentConfig.alipay.qrCode || "/images/alipay.png"
+ }
+
+ return (
+
+
+
+
+
+
+
{title}
+
+
+
支付金额
+
{displayAmount}
+ {(isCrypto || isPayPal) &&
≈ ¥{amount.toFixed(2)}
}
+
+
+ {(isWechat || isAlipay) ? (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
请使用{title === "微信支付" ? "微信" : "支付宝"}扫码支付
+
+ ) : (
+
+
+ {isCrypto ? `收款地址 (${paymentConfig.usdt.network})` : "PayPal账户"}
+
+
+
+ {address || "请联系客服获取"}
+
+ {address && (
+
+ )}
+
+
+ )}
+
+
+
+ 支付完成后,请点击下方"我已支付"按钮,
系统将自动开通阅读权限
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ {isSuccess ? (
+
+
+
+
+
支付成功
+
{type === "fullbook" ? "您已解锁全部内容" : "您已解锁本节内容"}
+
+ ) : (
+ <>
+
+
确认支付
+
+ {type === "fullbook" ? "购买整本书,解锁全部内容" : `购买: ${sectionTitle}`}
+
+
+
+
+
支付金额
+
¥{amount.toFixed(2)}
+ {(paymentMethod === "usdt" || paymentMethod === "paypal") && (
+
+ ≈ ${paymentMethod === "usdt" ? usdtAmount : paypalAmount} USD
+
+ )}
+ {user?.referredBy && (
+
+ 通过邀请注册,{settings?.distributorShare || 90}%将返还给推荐人
+
+ )}
+
+
+
+
选择支付方式
+ {availableMethods.map((method) => (
+
+ ))}
+ {availableMethods.length === 0 &&
暂无可用支付方式
}
+
+
+
+
+
支付即表示同意《用户协议》和《隐私政策》
+
+ >
+ )}
+
+
+ )
+}
diff --git a/components/modules/referral/poster-modal.tsx b/components/modules/referral/poster-modal.tsx
new file mode 100644
index 0000000..8ced849
--- /dev/null
+++ b/components/modules/referral/poster-modal.tsx
@@ -0,0 +1,76 @@
+"use client"
+
+import { X } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+interface PosterModalProps {
+ isOpen: boolean
+ onClose: () => void
+ referralLink: string
+ referralCode: string
+ nickname: string
+}
+
+export function PosterModal({ isOpen, onClose, referralLink, referralCode, nickname }: PosterModalProps) {
+ if (!isOpen) return null
+
+ // Use a public QR code API
+ const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(referralLink)}`
+
+ return (
+
+
+
+
+
+
+ {/* Poster Content */}
+
+ {/* Decorative circles */}
+
+
+
+
+ {/* Book Title */}
+
一场SOUL的
创业实验场
+
真实商业故事 · 55个案例 · 每日更新
+
+ {/* Cover Image Placeholder */}
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
+ {/* Recommender Info */}
+
+ 推荐人: {nickname}
+
+
+ {/* QR Code Section */}
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

+
+
长按识别二维码试读
+
邀请码: {referralCode}
+
+
+
+ {/* Footer Actions */}
+
+
+ 长按上方图片保存,或截图分享
+
+
+
+
+
+ )
+}
diff --git a/components/modules/referral/referral-share.tsx b/components/modules/referral/referral-share.tsx
new file mode 100644
index 0000000..6c2a643
--- /dev/null
+++ b/components/modules/referral/referral-share.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import { Share2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/lib/store"
+
+interface ReferralShareProps {
+ sectionTitle: string
+ fullBookPrice: number
+ distributorShare: number
+}
+
+export function ReferralShare({ sectionTitle, fullBookPrice, distributorShare }: ReferralShareProps) {
+ const { user } = useStore()
+
+ const handleShare = async () => {
+ const url = user?.referralCode ? `${window.location.href}?ref=${user.referralCode}` : window.location.href
+ const shareData = {
+ title: sectionTitle,
+ text: `来自Soul派对房的真实商业故事: ${sectionTitle}`,
+ 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) {
+ console.error("Error sharing:", error)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/components/modules/referral/withdrawal-modal.tsx b/components/modules/referral/withdrawal-modal.tsx
new file mode 100644
index 0000000..e6d15a8
--- /dev/null
+++ b/components/modules/referral/withdrawal-modal.tsx
@@ -0,0 +1,172 @@
+"use client"
+
+import { useState } from "react"
+import { X, Wallet, CheckCircle } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { useStore } from "@/lib/store"
+
+interface WithdrawalModalProps {
+ isOpen: boolean
+ onClose: () => void
+ availableAmount: number
+}
+
+export function WithdrawalModal({ isOpen, onClose, availableAmount }: WithdrawalModalProps) {
+ const { requestWithdrawal } = useStore()
+ const [amount, setAmount] = useState("")
+ const [method, setMethod] = useState<"wechat" | "alipay">("wechat")
+ const [account, setAccount] = useState("")
+ const [name, setName] = useState("")
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+
+ if (!isOpen) return null
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const amountNum = parseFloat(amount)
+ if (isNaN(amountNum) || amountNum <= 0 || amountNum > availableAmount) {
+ alert("请输入有效的提现金额")
+ return
+ }
+
+ if (!account || !name) {
+ alert("请填写完整的提现信息")
+ return
+ }
+
+ setIsSubmitting(true)
+
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 1000))
+
+ requestWithdrawal(amountNum, method, account, name)
+
+ setIsSubmitting(false)
+ setIsSuccess(true)
+ }
+
+ const handleClose = () => {
+ setIsSuccess(false)
+ setAmount("")
+ setAccount("")
+ setName("")
+ onClose()
+ }
+
+ return (
+
+
+
+
+
+
+ {isSuccess ? (
+
+
+
+
+
申请提交成功
+
+ 您的提现申请已提交,预计1-3个工作日内到账。
+
+
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/components/party-group-section.tsx b/components/party-group-section.tsx
new file mode 100644
index 0000000..346f9a0
--- /dev/null
+++ b/components/party-group-section.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import Image from "next/image"
+import { useStore } from "@/lib/store"
+
+export function PartyGroupSection() {
+ const { settings, getLiveQRCodeUrl } = useStore()
+
+ const handleJoin = () => {
+ const url = getLiveQRCodeUrl("party-group")
+ if (url) {
+ window.open(url, "_blank")
+ }
+ }
+
+ return (
+
+
+
+
+ 每天 {settings.authorInfo?.liveTime || "06:00-09:00"} · 免费分享
+
+
+ {/* QR Code - smaller */}
+
+
+
+
+
扫码加入派对群
+
长按识别二维码
+
+
+
+ )
+}
diff --git a/components/payment-modal.tsx b/components/payment-modal.tsx
new file mode 100644
index 0000000..03ba319
--- /dev/null
+++ b/components/payment-modal.tsx
@@ -0,0 +1,311 @@
+"use client"
+
+import type React from "react"
+import { useState } from "react"
+import { X, CheckCircle, Bitcoin, Globe, Copy, Check } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/lib/store"
+
+const WechatIcon = () => (
+
+)
+
+const AlipayIcon = () => (
+
+)
+
+type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal"
+
+interface PaymentModalProps {
+ isOpen: boolean
+ onClose: () => void
+ type: "section" | "fullbook"
+ sectionId?: string
+ sectionTitle?: string
+ amount: number
+ onSuccess: () => void
+}
+
+export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, amount, onSuccess }: PaymentModalProps) {
+ const [paymentMethod, setPaymentMethod] = useState("wechat")
+ const [isProcessing, setIsProcessing] = useState(false)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const [showPaymentDetails, setShowPaymentDetails] = useState(false)
+ const [copied, setCopied] = useState(false)
+
+ const { purchaseSection, purchaseFullBook, user, settings } = useStore()
+
+ const paymentConfig = settings?.paymentMethods || {
+ wechat: { enabled: true, qrCode: "", account: "" },
+ alipay: { enabled: true, qrCode: "", account: "" },
+ usdt: { enabled: true, network: "TRC20", address: "", exchangeRate: 7.2 },
+ paypal: { enabled: false, email: "", exchangeRate: 7.2 },
+ }
+
+ const usdtAmount = (amount / (paymentConfig.usdt.exchangeRate || 7.2)).toFixed(2)
+ const paypalAmount = (amount / (paymentConfig.paypal.exchangeRate || 7.2)).toFixed(2)
+
+ const handleCopyAddress = (address: string) => {
+ navigator.clipboard.writeText(address)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ const handlePayment = async () => {
+ if (paymentMethod === "usdt" || paymentMethod === "paypal") {
+ setShowPaymentDetails(true)
+ return
+ }
+
+ setIsProcessing(true)
+ await new Promise((resolve) => setTimeout(resolve, 1500))
+
+ let success = false
+ if (type === "section" && sectionId) {
+ success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
+ } else if (type === "fullbook") {
+ success = await purchaseFullBook(paymentMethod)
+ }
+
+ setIsProcessing(false)
+
+ if (success) {
+ setIsSuccess(true)
+ setTimeout(() => {
+ onSuccess()
+ onClose()
+ setIsSuccess(false)
+ }, 1500)
+ }
+ }
+
+ const confirmCryptoPayment = async () => {
+ setIsProcessing(true)
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+
+ let success = false
+ if (type === "section" && sectionId) {
+ success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
+ } else if (type === "fullbook") {
+ success = await purchaseFullBook(paymentMethod)
+ }
+
+ setIsProcessing(false)
+ setShowPaymentDetails(false)
+
+ if (success) {
+ setIsSuccess(true)
+ setTimeout(() => {
+ onSuccess()
+ onClose()
+ setIsSuccess(false)
+ }, 1500)
+ }
+ }
+
+ if (!isOpen) return null
+
+ const paymentMethods: {
+ id: PaymentMethod
+ name: string
+ icon: React.ReactNode
+ color: string
+ enabled: boolean
+ extra?: string
+ }[] = [
+ {
+ id: "wechat",
+ name: "微信支付",
+ icon: ,
+ color: "bg-[#07C160]",
+ enabled: paymentConfig.wechat.enabled,
+ },
+ {
+ id: "alipay",
+ name: "支付宝",
+ icon: ,
+ color: "bg-[#1677FF]",
+ enabled: paymentConfig.alipay.enabled,
+ },
+ {
+ id: "usdt",
+ name: `USDT (${paymentConfig.usdt.network || "TRC20"})`,
+ icon: ,
+ color: "bg-[#26A17B]",
+ enabled: paymentConfig.usdt.enabled,
+ extra: `≈ $${usdtAmount}`,
+ },
+ {
+ id: "paypal",
+ name: "PayPal",
+ icon: ,
+ color: "bg-[#003087]",
+ enabled: paymentConfig.paypal.enabled,
+ extra: `≈ $${paypalAmount}`,
+ },
+ ]
+
+ const availableMethods = paymentMethods.filter((m) => m.enabled)
+
+ // Crypto payment details view
+ if (showPaymentDetails) {
+ const isCrypto = paymentMethod === "usdt"
+ const address = isCrypto ? paymentConfig.usdt.address : paymentConfig.paypal.email
+ const displayAmount = isCrypto ? `$${usdtAmount} USDT` : `$${paypalAmount} USD`
+
+ return (
+
+
+
+
+
+
+
{isCrypto ? "USDT支付" : "PayPal支付"}
+
+
+
支付金额
+
{displayAmount}
+
≈ ¥{amount.toFixed(2)}
+
+
+
+
+ {isCrypto ? `收款地址 (${paymentConfig.usdt.network})` : "PayPal账户"}
+
+
+
+ {address || (isCrypto ? "请联系客服获取地址" : "请联系客服获取账户")}
+
+ {address && (
+
+ )}
+
+
+
+
+
+ 请在转账完成后点击"已完成支付"按钮,系统将在1-24小时内确认到账并开通权限。
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+ {isSuccess ? (
+
+
+
+
+
支付成功
+
{type === "fullbook" ? "您已解锁全部内容" : "您已解锁本节内容"}
+
+ ) : (
+ <>
+
+
确认支付
+
+ {type === "fullbook" ? "购买整本书,解锁全部内容" : `购买: ${sectionTitle}`}
+
+
+
+
+
支付金额
+
¥{amount.toFixed(2)}
+ {(paymentMethod === "usdt" || paymentMethod === "paypal") && (
+
+ ≈ ${paymentMethod === "usdt" ? usdtAmount : paypalAmount} USD
+
+ )}
+ {user?.referredBy && (
+
+ 通过邀请注册,{settings?.distributorShare || 90}%将返还给推荐人
+
+ )}
+
+
+
+
选择支付方式
+ {availableMethods.map((method) => (
+
+ ))}
+ {availableMethods.length === 0 &&
暂无可用支付方式
}
+
+
+
+
+
支付即表示同意《用户协议》和《隐私政策》
+
+ >
+ )}
+
+
+ )
+}
diff --git a/components/purchase-section.tsx b/components/purchase-section.tsx
new file mode 100644
index 0000000..d2cb5e7
--- /dev/null
+++ b/components/purchase-section.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Button } from "@/components/ui/button"
+import { Zap, BookOpen } from "lucide-react"
+import { getFullBookPrice, getAllSections } from "@/lib/book-data"
+import { useStore } from "@/lib/store"
+import { AuthModal } from "./modules/auth/auth-modal"
+import { PaymentModal } from "./modules/payment/payment-modal"
+
+export function PurchaseSection() {
+ const [fullBookPrice, setFullBookPrice] = useState(9.9)
+ const [sectionsCount, setSectionsCount] = useState(55)
+ const [isAuthOpen, setIsAuthOpen] = useState(false)
+ const [isPaymentOpen, setIsPaymentOpen] = useState(false)
+ const { isLoggedIn } = useStore()
+
+ useEffect(() => {
+ const sections = getAllSections()
+ setSectionsCount(sections.length)
+ setFullBookPrice(getFullBookPrice(sections.length))
+ }, [])
+
+ const handlePurchase = () => {
+ if (!isLoggedIn) {
+ setIsAuthOpen(true)
+ return
+ }
+ setIsPaymentOpen(true)
+ }
+
+ return (
+
+
+ {/* Pricing cards - stacked on mobile */}
+
+ {/* Single section */}
+
+
+ {/* Full book - highlighted */}
+
+
+ 推荐
+
+
+
+
+
+
+
整本购买
+
全部{sectionsCount}节 · 后续更新免费
+
+
+
+ ¥{fullBookPrice.toFixed(1)}
+
+
+
+
+
+
+
+ {/* Dynamic pricing note */}
+
动态定价: 每新增一章节,整本价格+¥1
+
+
+ setIsAuthOpen(false)} />
+ setIsPaymentOpen(false)}
+ type="fullbook"
+ amount={fullBookPrice}
+ onSuccess={() => window.location.reload()}
+ />
+
+ )
+}
diff --git a/components/qr-code-modal.tsx b/components/qr-code-modal.tsx
new file mode 100644
index 0000000..806e113
--- /dev/null
+++ b/components/qr-code-modal.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import { useState } from "react"
+import Image from "next/image"
+import { X, MessageCircle } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStore } from "@/lib/store"
+
+interface QRCodeModalProps {
+ isOpen: boolean
+ onClose: () => void
+}
+
+export function QRCodeModal({ isOpen, onClose }: QRCodeModalProps) {
+ const { settings, getLiveQRCodeUrl } = useStore()
+ const [isJoining, setIsJoining] = useState(false)
+
+ if (!isOpen) return null
+
+ const qrCodeImage = "/images/image.png"
+
+ const handleJoin = () => {
+ setIsJoining(true)
+ // 获取活码随机URL
+ const url = getLiveQRCodeUrl("party-group")
+ if (url) {
+ window.open(url, "_blank")
+ }
+ setTimeout(() => setIsJoining(false), 1000)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
继续学习
+
+ 每天早上{settings.authorInfo?.liveTime || "06:00-09:00"},{settings.authorInfo?.name || "卡若"}
+ 在派对房免费分享
+
+
+
+
+
+
+
扫码加入Soul派对群
+
+
+
+
+
+ )
+}
diff --git a/components/table-of-contents.tsx b/components/table-of-contents.tsx
new file mode 100644
index 0000000..5a45b7f
--- /dev/null
+++ b/components/table-of-contents.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import Link from "next/link"
+import { ChevronRight } from "lucide-react"
+import { Part } from "@/lib/book-data"
+
+interface TableOfContentsProps {
+ parts: Part[]
+}
+
+export function TableOfContents({ parts }: TableOfContentsProps) {
+ return (
+
+
+ {/* Section title */}
+
全书 {parts.length} 篇
+
+ {/* Parts list */}
+
+ {parts.map((part) => (
+
+
+
+
+
{part.number}
+
+
+ {part.title}
+
+
{part.subtitle}
+
+ {part.chapters.length} 章 · {part.chapters.reduce((acc, c) => acc + c.sections.length, 0)} 节
+
+
+
+
+
+
+
+ ))}
+
+
+ {/* Additional content */}
+
+
+
+
+
序言
+
+ 为什么我每天早上6点在Soul开播?
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..f64632d
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,60 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost:
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
+ 'icon-sm': 'size-8',
+ 'icon-lg': 'size-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : 'button'
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..77e9fb7
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..f199a06
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/components/ui/label.tsx b/components/ui/label.tsx
new file mode 100644
index 0000000..4d0ef28
--- /dev/null
+++ b/components/ui/label.tsx
@@ -0,0 +1,22 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cn } from "@/lib/utils"
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
new file mode 100644
index 0000000..cbe5a36
--- /dev/null
+++ b/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 0000000..b17bb50
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,26 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "horizontal", ...props }, ref) => (
+
+))
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 0000000..01b8b6d
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000..eb2e0d2
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
new file mode 100644
index 0000000..7f3502f
--- /dev/null
+++ b/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Table.displayName = "Table"
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableHeader.displayName = "TableHeader"
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableBody.displayName = "TableBody"
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+))
+TableFooter.displayName = "TableFooter"
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableRow.displayName = "TableRow"
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableHead.displayName = "TableHead"
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableCell.displayName = "TableCell"
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = "TableCaption"
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..39b1921
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,54 @@
+"use client"
+
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..4466dc6
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,20 @@
+import * as React from "react"
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef>(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/components/user-menu.tsx b/components/user-menu.tsx
new file mode 100644
index 0000000..cd8014a
--- /dev/null
+++ b/components/user-menu.tsx
@@ -0,0 +1,107 @@
+"use client"
+
+import { useState } from "react"
+import Link from "next/link"
+import { User, LogOut, BookOpen, Gift, Settings } from "lucide-react"
+import { useStore } from "@/lib/store"
+import { AuthModal } from "./modules/auth/auth-modal"
+
+export function UserMenu() {
+ const [isAuthOpen, setIsAuthOpen] = useState(false)
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
+ const { user, isLoggedIn, logout } = useStore()
+
+ if (!isLoggedIn || !user) {
+ return (
+ <>
+
+ setIsAuthOpen(false)} />
+ >
+ )
+ }
+
+ return (
+
+
+
+ {isMenuOpen && (
+ <>
+
setIsMenuOpen(false)} />
+
+ {/* User info */}
+
+
{user.nickname}
+
{user.phone}
+ {user.hasFullBook && (
+
+ 已购买全书
+
+ )}
+
+
+ {/* Menu items */}
+
+ setIsMenuOpen(false)}
+ >
+
+ 我的购买
+
+ setIsMenuOpen(false)}
+ >
+
+ 分销中心
+ {user.earnings > 0 && (
+ ¥{user.earnings.toFixed(2)}
+ )}
+
+ {user.isAdmin && (
+ setIsMenuOpen(false)}
+ >
+
+ 管理后台
+
+ )}
+
+
+ {/* Logout */}
+
+
+
+
+ >
+ )}
+
+ )
+}