From 0bee46d58d976e6cda799f93ea6f196699e955ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E8=8B=A5?= Date: Mon, 29 Dec 2025 19:06:29 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=81=A2=E5=A4=8D=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=20components=20=E7=9B=AE=E5=BD=95=E5=88=B0=20GitHub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - components/auth-modal.tsx | 225 ++++++++++++ components/book-cover.tsx | 111 ++++++ components/book-intro.tsx | 34 ++ components/bottom-nav.tsx | 62 ++++ components/buy-full-book-button.tsx | 56 +++ components/chapter-content.tsx | 290 +++++++++++++++ components/chapters-list.tsx | 135 +++++++ components/config-loader.tsx | 14 + components/footer.tsx | 12 + components/layout-wrapper.tsx | 27 ++ components/modules/auth/auth-modal.tsx | 225 ++++++++++++ .../modules/marketing/qr-code-modal.tsx | 123 +++++++ components/modules/payment/payment-modal.tsx | 343 ++++++++++++++++++ components/modules/referral/poster-modal.tsx | 76 ++++ .../modules/referral/referral-share.tsx | 48 +++ .../modules/referral/withdrawal-modal.tsx | 172 +++++++++ components/party-group-section.tsx | 35 ++ components/payment-modal.tsx | 311 ++++++++++++++++ components/purchase-section.tsx | 96 +++++ components/qr-code-modal.tsx | 78 ++++ components/table-of-contents.tsx | 65 ++++ components/theme-provider.tsx | 11 + components/ui/button.tsx | 60 +++ components/ui/card.tsx | 76 ++++ components/ui/input.tsx | 21 ++ components/ui/label.tsx | 22 ++ components/ui/select.tsx | 160 ++++++++ components/ui/separator.tsx | 26 ++ components/ui/skeleton.tsx | 15 + components/ui/switch.tsx | 28 ++ components/ui/table.tsx | 117 ++++++ components/ui/tabs.tsx | 54 +++ components/ui/textarea.tsx | 20 + components/user-menu.tsx | 107 ++++++ 35 files changed, 3255 insertions(+), 1 deletion(-) create mode 100644 components/auth-modal.tsx create mode 100644 components/book-cover.tsx create mode 100644 components/book-intro.tsx create mode 100644 components/bottom-nav.tsx create mode 100644 components/buy-full-book-button.tsx create mode 100644 components/chapter-content.tsx create mode 100644 components/chapters-list.tsx create mode 100644 components/config-loader.tsx create mode 100644 components/footer.tsx create mode 100644 components/layout-wrapper.tsx create mode 100644 components/modules/auth/auth-modal.tsx create mode 100644 components/modules/marketing/qr-code-modal.tsx create mode 100644 components/modules/payment/payment-modal.tsx create mode 100644 components/modules/referral/poster-modal.tsx create mode 100644 components/modules/referral/referral-share.tsx create mode 100644 components/modules/referral/withdrawal-modal.tsx create mode 100644 components/party-group-section.tsx create mode 100644 components/payment-modal.tsx create mode 100644 components/purchase-section.tsx create mode 100644 components/qr-code-modal.tsx create mode 100644 components/table-of-contents.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/user-menu.tsx 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 */} +
+
+

作者

+

卡若

+
+
+

每日直播

+

06:00-09:00

+
+
+ + {/* 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 */} +
+
+

55+

+

真实案例

+
+
+

11

+

核心章节

+
+
+

100+

+

商业洞察

+
+
+
+
+
+ ) +} 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 ( +
+
+

一场SOUL的创业实验场

+

© 2025 卡若 · 每日直播 06:00-09:00

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

请使用{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 */} + Book Cover +
+ + {/* Recommender Info */} +
+ 推荐人: {nickname} +
+ + {/* QR Code Section */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + QR Code +
+

长按识别二维码试读

+

邀请码: {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个工作日内到账。 +

+ +
+ ) : ( +
+
+ +

申请提现

+
+ +
+
+ +
+ ¥ + setAmount(e.target.value)} + className="pl-7" + placeholder="最低10元" + /> +
+
+ +
+ +
+ + +
+
+ +
+ + setAccount(e.target.value)} + placeholder={method === "wechat" ? "请输入微信号" : "请输入支付宝账号"} + /> +
+ +
+ + setName(e.target.value)} + placeholder="请输入收款人真实姓名" + /> +
+
+ + +
+ )} +
+
+ ) +} 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 */} +
+
+
+ +
+

单节购买

+

按兴趣选择

+
+
+
+ ¥1 + /节 +
+
+
+ + {/* 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 ( +