Add EPUB upload + DB integration
Add server-side EPUB import and integrate Prisma + Mongo for novels/chapters. Introduces a new moderator API route (app/api/mod/epub/route.ts) that parses .epub files, creates a novel record in Prisma, and inserts chapter documents into MongoDB via the Chapter Mongoose model. Frontend: novel management UI now supports EPUB upload (app/mod/truyen/novel-client.tsx) with progress/toasts and preserves the manual 'Add novel' dialog. Convert app pages to fetch real data from Prisma and Mongo (app/page.tsx, app/truyen/[slug]/page.tsx, app/truyen/[slug]/[chapterId]/page.tsx), adapt types/props to use authorName, and adjust chapter/comment IDs to use Mongo _id strings. Minor fixes: TTS player logs playback errors, UI text fixes (e.g. "Chương"), and novel-card/other components updated for authorName. package.json updated with epub2, html-to-text and types; pnpm lock updated. Adds tsconfig.tsbuildinfo.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { promises as fs } from "fs"
|
||||
import { convert } from "html-to-text"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = await req.formData()
|
||||
const epubFile = formData.get("file") as File
|
||||
if (!epubFile) {
|
||||
return NextResponse.json({ error: "Thiếu file EPUB" }, { status: 400 })
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await epubFile.arrayBuffer())
|
||||
const tempFilePath = path.join(os.tmpdir(), `upload-${Date.now()}.epub`)
|
||||
await fs.writeFile(tempFilePath, buffer)
|
||||
|
||||
// Phân tích EPUB file
|
||||
const parsedData = await new Promise<any>((resolve, reject) => {
|
||||
const EPub = require("epub2").EPub || require("epub2")
|
||||
const epub = new EPub(tempFilePath, "", "")
|
||||
|
||||
epub.on("error", (err: any) => reject(err))
|
||||
epub.on("end", async () => {
|
||||
const metadata = epub.metadata
|
||||
const flow = epub.flow // TOC array
|
||||
const chapters = []
|
||||
|
||||
for (let i = 0; i < flow.length; i++) {
|
||||
const chapterData = flow[i]
|
||||
const text = await new Promise<string>((res) => {
|
||||
epub.getChapter(chapterData.id, (err: any, d: string) => {
|
||||
if (err) res("")
|
||||
else res(d)
|
||||
})
|
||||
})
|
||||
|
||||
if (text && text.trim().length > 0) {
|
||||
const plainText = convert(text, { wordwrap: false })
|
||||
chapters.push({
|
||||
title: chapterData.title || `Chương ${i + 1}`,
|
||||
content: plainText
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolve({ metadata, chapters })
|
||||
})
|
||||
|
||||
epub.parse()
|
||||
})
|
||||
|
||||
// Xóa file tạm
|
||||
await fs.unlink(tempFilePath).catch(() => { })
|
||||
|
||||
const { metadata, chapters } = parsedData
|
||||
|
||||
let novelTitle = metadata.title || "Truyện chưa đặt tên"
|
||||
let novelAuthor = metadata.creator || "Khuyết danh"
|
||||
let novelDesc = metadata.description || "Chưa có giới thiệu"
|
||||
|
||||
// Generate base slug
|
||||
const baseSlug = novelTitle
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)+/g, "")
|
||||
|
||||
let slug = baseSlug
|
||||
let slugCounter = 1
|
||||
|
||||
// Đảm bảo slug là duy nhất
|
||||
while (await prisma.novel.findUnique({ where: { slug } })) {
|
||||
slug = `${baseSlug}-${slugCounter}`
|
||||
slugCounter++
|
||||
}
|
||||
|
||||
const newNovel = await prisma.novel.create({
|
||||
data: {
|
||||
title: novelTitle,
|
||||
slug: slug,
|
||||
authorName: novelAuthor,
|
||||
description: convert(novelDesc, { wordwrap: false }), // metadata metadata có thể chứa html
|
||||
uploaderId: session.user.id,
|
||||
totalChapters: chapters.length,
|
||||
},
|
||||
})
|
||||
|
||||
// Lưu chapters xuống MongoDB
|
||||
await connectToMongoDB()
|
||||
const chapterDocs = chapters.map((ch: any, i: number) => ({
|
||||
novelId: newNovel.id,
|
||||
number: i + 1,
|
||||
title: ch.title,
|
||||
content: ch.content,
|
||||
views: 0
|
||||
}))
|
||||
|
||||
if (chapterDocs.length > 0) {
|
||||
await Chapter.insertMany(chapterDocs)
|
||||
}
|
||||
|
||||
return NextResponse.json(newNovel, { status: 201 })
|
||||
} catch (error: any) {
|
||||
console.error("EPUB upload error:", error)
|
||||
return NextResponse.json({ error: "Lỗi xử lý file EPUB", details: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { BookOpen, Loader2, Plus } from "lucide-react"
|
||||
import { BookOpen, Loader2, Plus, Upload } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import Link from "next/link"
|
||||
|
||||
@@ -31,6 +31,7 @@ export function NovelClient() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [openAdd, setOpenAdd] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [uploadingEpub, setUploadingEpub] = useState(false)
|
||||
|
||||
// Form states
|
||||
const [title, setTitle] = useState("")
|
||||
@@ -82,6 +83,41 @@ export function NovelClient() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEpubUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.name.endsWith('.epub')) {
|
||||
toast.error("Vui lòng chọn file định dạng .epub")
|
||||
e.target.value = "" // Reset input
|
||||
return
|
||||
}
|
||||
|
||||
setUploadingEpub(true)
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/mod/epub", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error || "Lỗi khi tải lên EPUB")
|
||||
}
|
||||
|
||||
toast.success("Phân tích và xuất bản EPUB thành công!")
|
||||
fetchNovels()
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || "Có lỗi xảy ra khi xử lý file EPUB")
|
||||
} finally {
|
||||
setUploadingEpub(false)
|
||||
e.target.value = "" // Reset input
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center bg-card p-4 rounded-xl border shadow-sm">
|
||||
@@ -89,41 +125,61 @@ export function NovelClient() {
|
||||
<BookOpen className="h-6 w-6 text-primary" /> Quản lý truyện
|
||||
</h1>
|
||||
|
||||
<Dialog open={openAdd} onOpenChange={setOpenAdd}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Thêm truyện
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thêm Truyện Mới</DialogTitle>
|
||||
<DialogDescription>
|
||||
Nhập thông tin cơ bản cho đầu truyện mới của bạn.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleAddSubmit} className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tên truyện</label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Ví dụ: Phàm Nhân Tu Tiên" autoFocus />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tác giả gốc</label>
|
||||
<Input value={authorName} onChange={(e) => setAuthorName(e.target.value)} placeholder="Ví dụ: Vong Ngữ" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Giới thiệu ngắn (Mô tả)</label>
|
||||
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Tóm tắt về câu chuyện..." rows={4} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Hoàn thành
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="file"
|
||||
id="epub-upload"
|
||||
accept=".epub,application/epub+zip"
|
||||
className="hidden"
|
||||
onChange={handleEpubUpload}
|
||||
disabled={uploadingEpub}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="gap-2"
|
||||
disabled={uploadingEpub}
|
||||
onClick={() => document.getElementById('epub-upload')?.click()}
|
||||
>
|
||||
{uploadingEpub ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
|
||||
Tải lên EPUB
|
||||
</Button>
|
||||
|
||||
<Dialog open={openAdd} onOpenChange={setOpenAdd}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Thêm truyện
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Thêm Truyện Mới</DialogTitle>
|
||||
<DialogDescription>
|
||||
Nhập thông tin cơ bản cho đầu truyện mới của bạn.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleAddSubmit} className="space-y-4 pt-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tên truyện</label>
|
||||
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Ví dụ: Phàm Nhân Tu Tiên" autoFocus />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Tác giả gốc</label>
|
||||
<Input value={authorName} onChange={(e) => setAuthorName(e.target.value)} placeholder="Ví dụ: Vong Ngữ" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Giới thiệu ngắn (Mô tả)</label>
|
||||
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Tóm tắt về câu chuyện..." rows={4} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Hoàn thành
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-card overflow-hidden shadow-sm">
|
||||
|
||||
+57
-41
@@ -1,7 +1,8 @@
|
||||
import Link from "next/link"
|
||||
import { ArrowRight, BookOpen, Sparkles, Flame, Heart, Swords, Building2, Rocket, Crown, Laugh, Search, Shield } from "lucide-react"
|
||||
import { NovelCard } from "@/components/novel-card"
|
||||
import { genres, getPopularNovels, getLatestNovels, getTopRatedNovels, novels } from "@/lib/data"
|
||||
import { genres } from "@/lib/data"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
Sparkles: <Sparkles className="h-5 w-5" />,
|
||||
@@ -16,43 +17,58 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
Shield: <Shield className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const popularNovels = getPopularNovels(6)
|
||||
const latestNovels = getLatestNovels(6)
|
||||
const topRated = getTopRatedNovels(4)
|
||||
const featured = novels[3] // Anh Hung Xa Dieu
|
||||
export default async function HomePage() {
|
||||
const popularNovels = await prisma.novel.findMany({
|
||||
take: 6,
|
||||
orderBy: { views: "desc" },
|
||||
})
|
||||
|
||||
const latestNovels = await prisma.novel.findMany({
|
||||
take: 6,
|
||||
orderBy: { updatedAt: "desc" },
|
||||
})
|
||||
|
||||
const topRated = await prisma.novel.findMany({
|
||||
take: 4,
|
||||
orderBy: { rating: "desc" },
|
||||
})
|
||||
|
||||
// get the most popular as featured (can be empty if DB is new)
|
||||
const featured = popularNovels[0]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-6">
|
||||
{/* Hero / Featured Novel */}
|
||||
<section className="mb-10">
|
||||
<Link
|
||||
href={`/truyen/${featured.slug}`}
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-border bg-card md:flex-row"
|
||||
>
|
||||
<div className={`flex h-48 items-center justify-center bg-gradient-to-br ${featured.coverColor} md:h-auto md:w-72`}>
|
||||
<BookOpen className="h-16 w-16 text-background/80" />
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-center gap-3 p-6">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-primary">Truyện Nổi Bật</span>
|
||||
<h1 className="text-2xl font-bold text-foreground group-hover:text-primary transition-colors text-balance md:text-3xl">
|
||||
{featured.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Tác giả: {featured.author}</p>
|
||||
<p className="line-clamp-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{featured.totalChapters} chương</span>
|
||||
<span>{featured.status}</span>
|
||||
<span className="flex items-center gap-1 text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{featured.rating}
|
||||
</span>
|
||||
{featured && (
|
||||
<section className="mb-10">
|
||||
<Link
|
||||
href={`/truyen/${featured.slug}`}
|
||||
className="group relative flex flex-col overflow-hidden rounded-xl border border-border bg-card md:flex-row"
|
||||
>
|
||||
<div className={`flex h-48 items-center justify-center bg-gradient-to-br ${featured.coverColor || "from-slate-700 to-slate-800"} md:h-auto md:w-72`}>
|
||||
<BookOpen className="h-16 w-16 text-background/80" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
<div className="flex flex-1 flex-col justify-center gap-3 p-6">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-primary">Truyện Nổi Bật</span>
|
||||
<h1 className="text-2xl font-bold text-foreground group-hover:text-primary transition-colors text-balance md:text-3xl">
|
||||
{featured.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Tác giả: {featured.authorName}</p>
|
||||
<p className="line-clamp-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{featured.totalChapters} chương</span>
|
||||
<span>{featured.status}</span>
|
||||
<span className="flex items-center gap-1 text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{featured.rating}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Popular Novels */}
|
||||
<section className="mb-10">
|
||||
@@ -63,9 +79,9 @@ export default function HomePage() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{popularNovels.map((novel) => (
|
||||
{popularNovels.length > 0 ? popularNovels.map((novel) => (
|
||||
<NovelCard key={novel.id} novel={novel} />
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground col-span-full">Chưa có truyện nào trong hệ thống.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -78,9 +94,9 @@ export default function HomePage() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{latestNovels.map((novel) => (
|
||||
{latestNovels.length > 0 ? latestNovels.map((novel) => (
|
||||
<NovelCard key={novel.id} novel={novel} variant="compact" />
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground col-span-full">Chưa có truyện nào được cập nhật.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,7 +106,7 @@ export default function HomePage() {
|
||||
<section>
|
||||
<h2 className="mb-4 text-xl font-bold text-foreground">Đánh Giá Cao</h2>
|
||||
<div className="flex flex-col gap-3">
|
||||
{topRated.map((novel, idx) => (
|
||||
{topRated.length > 0 ? topRated.map((novel, idx) => (
|
||||
<Link
|
||||
key={novel.id}
|
||||
href={`/truyen/${novel.slug}`}
|
||||
@@ -99,19 +115,19 @@ export default function HomePage() {
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-bold text-primary">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div className={`flex h-12 w-9 shrink-0 items-center justify-center rounded bg-gradient-to-br ${novel.coverColor}`}>
|
||||
<div className={`flex h-12 w-9 shrink-0 items-center justify-center rounded bg-gradient-to-br ${novel.coverColor || "from-slate-700 to-slate-800"}`}>
|
||||
<BookOpen className="h-4 w-4 text-background/80" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate text-sm font-semibold text-foreground group-hover:text-primary transition-colors">{novel.title}</h3>
|
||||
<p className="text-xs text-muted-foreground">{novel.author} - Ch. {novel.totalChapters}</p>
|
||||
<p className="text-xs text-muted-foreground">{novel.authorName} - Ch. {novel.totalChapters}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm font-semibold text-primary">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{novel.rating}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
)) : <p className="text-sm text-muted-foreground">Chưa có đánh giá.</p>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
"use client"
|
||||
|
||||
import { use, useMemo } from "react"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { ChevronLeft, ChevronRight, List } from "lucide-react"
|
||||
import { getNovelBySlug, getChapter, getChaptersByNovelId, getCommentsByChapterId } from "@/lib/data"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ReadingSettings } from "@/components/reading-settings"
|
||||
import { CommentSection } from "@/components/comment-section"
|
||||
import { TTSPlayer } from "@/components/tts-player"
|
||||
import { ChapterReaderProgress } from "./chapter-reader-progress"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter as ChapterModel } from "@/lib/models/chapter"
|
||||
|
||||
export default function ChapterReaderPage({ params }: { params: Promise<{ slug: string; chapterId: string }> }) {
|
||||
const { slug, chapterId } = use(params)
|
||||
export default async function ChapterReaderPage({ params }: { params: Promise<{ slug: string; chapterId: string }> }) {
|
||||
const { slug, chapterId } = await params
|
||||
const chapterNumber = parseInt(chapterId, 10)
|
||||
const novel = getNovelBySlug(slug)
|
||||
|
||||
if (!novel || isNaN(chapterNumber)) {
|
||||
if (isNaN(chapterNumber)) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const chapter = getChapter(novel.id, chapterNumber)
|
||||
const allChapters = getChaptersByNovelId(novel.id)
|
||||
const maxChapter = allChapters.length
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: { slug }
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
await connectToMongoDB()
|
||||
const chapter = await ChapterModel.findOne({ novelId: novel.id, number: chapterNumber }).lean()
|
||||
|
||||
if (!chapter) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const comments = getCommentsByChapterId(chapter.id)
|
||||
const maxChapter = await ChapterModel.countDocuments({ novelId: novel.id })
|
||||
const comments: any[] = [] // Temporarily empty
|
||||
|
||||
const hasPrev = chapterNumber > 1
|
||||
const hasNext = chapterNumber < maxChapter
|
||||
|
||||
// Extract paragraphs for TTS
|
||||
const paragraphs = useMemo(
|
||||
() => chapter.content.split("\n").map((p) => p.trim()).filter(Boolean),
|
||||
[chapter.content]
|
||||
)
|
||||
const paragraphs = chapter.content.split("\n").map((p: string) => p.trim()).filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-6">
|
||||
@@ -85,7 +89,7 @@ export default function ChapterReaderPage({ params }: { params: Promise<{ slug:
|
||||
|
||||
{/* Chapter content */}
|
||||
<article className="chapter-content mb-8 rounded-lg border border-border bg-card p-6 font-serif text-foreground/90 md:p-8">
|
||||
{paragraphs.map((text, idx) => (
|
||||
{paragraphs.map((text: string, idx: number) => (
|
||||
<p key={idx} data-p-index={idx} className="mb-4 last:mb-0">
|
||||
{text}
|
||||
</p>
|
||||
@@ -115,11 +119,11 @@ export default function ChapterReaderPage({ params }: { params: Promise<{ slug:
|
||||
</div>
|
||||
|
||||
{/* Save reading progress */}
|
||||
<ChapterReaderProgress novelId={novel.id} chapterId={chapter.id} chapterNumber={chapter.number} />
|
||||
<ChapterReaderProgress novelId={novel.id} chapterId={chapter._id.toString()} chapterNumber={chapter.number} />
|
||||
|
||||
{/* Comments */}
|
||||
<section className="border-t border-border pt-8">
|
||||
<CommentSection comments={comments} novelId={novel.id} chapterId={chapter.id} />
|
||||
<CommentSection comments={comments} novelId={novel.id} chapterId={chapter._id.toString()} />
|
||||
</section>
|
||||
|
||||
{/* TTS Player */}
|
||||
@@ -128,7 +132,7 @@ export default function ChapterReaderPage({ params }: { params: Promise<{ slug:
|
||||
novelSlug={slug}
|
||||
currentChapter={chapterNumber}
|
||||
maxChapter={maxChapter}
|
||||
chapterTitle={`Chuong ${chapter.number}: ${chapter.title}`}
|
||||
chapterTitle={`Chương ${chapter.number}: ${chapter.title}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
+43
-22
@@ -1,36 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import { use } from "react"
|
||||
import { notFound } from "next/navigation"
|
||||
import { BookOpen, Eye, BookMarked, User, Clock, Layers } from "lucide-react"
|
||||
import { getNovelBySlug, getChaptersByNovelId, getCommentsByNovelId, genres, formatViews } from "@/lib/data"
|
||||
import { genres, formatViews } from "@/lib/data"
|
||||
import { GenreBadge } from "@/components/genre-badge"
|
||||
import { StarRating } from "@/components/star-rating"
|
||||
import { ChapterList } from "@/components/chapter-list"
|
||||
import { CommentSection } from "@/components/comment-section"
|
||||
import { NovelDetailActions } from "./novel-detail-actions"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
|
||||
export default function NovelDetailPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = use(params)
|
||||
const novel = getNovelBySlug(slug)
|
||||
export default async function NovelDetailPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
genres: {
|
||||
include: { genre: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const chapters = getChaptersByNovelId(novel.id)
|
||||
const comments = getCommentsByNovelId(novel.id)
|
||||
// Fetch chapters from MongoDB
|
||||
await connectToMongoDB()
|
||||
const chapters = await Chapter.find({ novelId: novel.id })
|
||||
.sort({ number: 1 })
|
||||
.select("id novelId number title createdAt views")
|
||||
.lean()
|
||||
|
||||
const novelGenres = novel.genres
|
||||
.map((gSlug) => genres.find((g) => g.slug === gSlug))
|
||||
.filter(Boolean) as typeof genres
|
||||
// Convert Mongoose documents to plain objects for Server Component
|
||||
const formattedChapters = chapters.map(c => ({
|
||||
id: c._id.toString(),
|
||||
novelId: c.novelId,
|
||||
number: c.number,
|
||||
title: c.title,
|
||||
createdAt: (c.createdAt as Date).toISOString(),
|
||||
views: c.views || 0,
|
||||
content: "" // We don't fetch content for the list
|
||||
}))
|
||||
|
||||
const comments: any[] = [] // Temporarily empty until we implement comments
|
||||
const novelGenres = novel.genres.map(ng => ng.genre) || []
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-4 py-6">
|
||||
{/* Novel Header */}
|
||||
<div className="flex flex-col gap-6 md:flex-row">
|
||||
{/* Cover */}
|
||||
<div className={`flex h-64 w-44 shrink-0 items-center justify-center self-center rounded-xl bg-gradient-to-br shadow-lg md:self-start ${novel.coverColor}`}>
|
||||
<div className={`flex h-64 w-44 shrink-0 items-center justify-center self-center rounded-xl bg-gradient-to-br shadow-lg md:self-start ${novel.coverColor || "from-slate-700 to-slate-800"}`}>
|
||||
<BookOpen className="h-14 w-14 text-background/80" />
|
||||
</div>
|
||||
|
||||
@@ -39,19 +61,18 @@ export default function NovelDetailPage({ params }: { params: Promise<{ slug: st
|
||||
<h1 className="text-2xl font-bold text-foreground text-balance md:text-3xl">{novel.title}</h1>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1"><User className="h-3.5 w-3.5" />{novel.author}</span>
|
||||
<span className="flex items-center gap-1"><User className="h-3.5 w-3.5" />{novel.authorName}</span>
|
||||
<span className="flex items-center gap-1"><Layers className="h-3.5 w-3.5" />{novel.totalChapters} chương</span>
|
||||
<span className="flex items-center gap-1"><Eye className="h-3.5 w-3.5" />{formatViews(novel.views)} lượt xem</span>
|
||||
<span className="flex items-center gap-1"><BookMarked className="h-3.5 w-3.5" />{formatViews(novel.bookmarkCount)} bookmark</span>
|
||||
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" />Cập nhật: {novel.lastUpdated}</span>
|
||||
<span className="flex items-center gap-1"><Clock className="h-3.5 w-3.5" />Cập nhật: {novel.updatedAt.toLocaleDateString('vi-VN')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${
|
||||
novel.status === "Hoàn thành" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
|
||||
<span className={`rounded-full px-2.5 py-0.5 text-xs font-semibold ${novel.status === "Hoàn thành" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
|
||||
novel.status === "Đang ra" ? "bg-primary/10 text-primary" :
|
||||
"bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
"bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{novel.status}
|
||||
</span>
|
||||
</div>
|
||||
@@ -64,7 +85,7 @@ export default function NovelDetailPage({ params }: { params: Promise<{ slug: st
|
||||
))}
|
||||
</div>
|
||||
|
||||
<NovelDetailActions novelId={novel.id} novelSlug={novel.slug} firstChapterNumber={chapters[0]?.number} />
|
||||
<NovelDetailActions novelId={novel.id} novelSlug={novel.slug} firstChapterNumber={formattedChapters[0]?.number} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,13 +99,13 @@ export default function NovelDetailPage({ params }: { params: Promise<{ slug: st
|
||||
<section className="mt-8">
|
||||
<h2 className="mb-3 text-lg font-bold text-foreground">Danh Sách Chương</h2>
|
||||
<div className="rounded-lg border border-border bg-card">
|
||||
<ChapterList chapters={chapters} novelSlug={novel.slug} />
|
||||
<ChapterList chapters={formattedChapters as any} novelSlug={novel.slug} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Comments */}
|
||||
<section className="mt-8">
|
||||
<CommentSection comments={comments} novelId={novel.id} />
|
||||
<CommentSection comments={comments as any} novelId={novel.id} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function BookshelfPage() {
|
||||
<Link href={`/truyen/${novel.slug}`} className="truncate text-sm font-semibold text-foreground hover:text-primary transition-colors">
|
||||
{novel.title}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground">{novel.author}</p>
|
||||
<p className="text-xs text-muted-foreground">{novel.authorName}</p>
|
||||
{bookmark.lastChapterNumber && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Đang đọc: Chương {bookmark.lastChapterNumber} / {novel.totalChapters}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import Link from "next/link"
|
||||
import { BookOpen, Eye, Star } from "lucide-react"
|
||||
import type { Novel } from "@/lib/types"
|
||||
import { formatViews } from "@/lib/data"
|
||||
|
||||
export interface CardNovel {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
authorName: string
|
||||
coverColor: string | null
|
||||
rating: number
|
||||
views: number
|
||||
totalChapters: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface NovelCardProps {
|
||||
novel: Novel
|
||||
novel: CardNovel
|
||||
variant?: "default" | "compact"
|
||||
}
|
||||
|
||||
@@ -22,7 +33,7 @@ export function NovelCard({ novel, variant = "default" }: NovelCardProps) {
|
||||
<h3 className="truncate text-sm font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
{novel.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">{novel.author}</p>
|
||||
<p className="text-xs text-muted-foreground">{novel.authorName}</p>
|
||||
<div className="mt-1 flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Star className="h-3 w-3 fill-primary text-primary" />
|
||||
@@ -52,7 +63,7 @@ export function NovelCard({ novel, variant = "default" }: NovelCardProps) {
|
||||
<h3 className="line-clamp-1 text-sm font-semibold text-foreground group-hover:text-primary transition-colors text-balance">
|
||||
{novel.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">{novel.author}</p>
|
||||
<p className="text-xs text-muted-foreground">{novel.authorName}</p>
|
||||
<div className="mt-auto flex items-center gap-3 pt-2 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Star className="h-3 w-3 fill-primary text-primary" />
|
||||
|
||||
@@ -166,6 +166,7 @@ export function TTSPlayer({ paragraphs, novelSlug, currentChapter, maxChapter, c
|
||||
}
|
||||
|
||||
utterance.onerror = (e) => {
|
||||
console.error("TTS Playback Error:", e.error, e)
|
||||
if (e.error !== "canceled" && e.error !== "interrupted") {
|
||||
setIsPlaying(false)
|
||||
releaseWakeLock()
|
||||
|
||||
+13
-13
@@ -20,7 +20,7 @@ export const novels: Novel[] = [
|
||||
id: "1",
|
||||
title: "Phàm Nhân Tu Tiên",
|
||||
slug: "pham-nhan-tu-tien",
|
||||
author: "Vong Ngữ",
|
||||
authorName: "Vong Ngữ",
|
||||
coverColor: "from-amber-500 to-orange-600",
|
||||
description: "Hàn Lập, một thiếu niên nghèo khó từ một ngôi làng nhỏ, tình cờ bước vào con đường tu tiên. Không có thiên phú xuất chúng, không có bối cảnh gia thế, chỉ bằng sự kiên trì và trí tuệ phi thường, hắn từng bước vượt qua muôn vàn khó khăn, chiến đấu với yêu ma quỷ quái, đối đầu với các thế lực lớn trong tu chân giới. Từ một phàm nhân bình thường, Hàn Lập dần dần khám phá ra bí mật của thiên địa, tìm kiếm con đường trường sinh bất lão.",
|
||||
genres: ["tien-hiep", "huyen-huyen"],
|
||||
@@ -37,7 +37,7 @@ export const novels: Novel[] = [
|
||||
id: "2",
|
||||
title: "Đấu Phá Thương Khung",
|
||||
slug: "dau-pha-thuong-khung",
|
||||
author: "Thiên Tằm Thổ Đậu",
|
||||
authorName: "Thiên Tằm Thổ Đậu",
|
||||
coverColor: "from-blue-500 to-indigo-600",
|
||||
description: "Tiêu Viêm, từng là thiên tài trẻ tuổi nhất Ô Thản thành, bỗng nhiên mất đi toàn bộ đấu khí vào năm 11 tuổi. Ba năm sau, cậu tình cờ khám phá ra bí mật ẩn giấu trong chiếc nhẫn truyền gia, từ đó bắt đầu hành trình tu luyện phi thường. Với sự giúp đỡ của Dược Lão, Tiêu Viêm quyết tâm lấy lại vinh quang đã mất và chinh phục đỉnh cao của thế giới đấu khí.",
|
||||
genres: ["huyen-huyen", "tien-hiep"],
|
||||
@@ -54,7 +54,7 @@ export const novels: Novel[] = [
|
||||
id: "3",
|
||||
title: "Hoa Thiên Cốt",
|
||||
slug: "hoa-thien-cot",
|
||||
author: "Fresh Quả Quả",
|
||||
authorName: "Fresh Quả Quả",
|
||||
coverColor: "from-pink-400 to-rose-500",
|
||||
description: "Hoa Thiên Cốt kể về câu chuyện tình yêu xuyên suốt ba kiếp giữa Hoa Thiên Cốt và Bạch Tử Họa. Nàng là đệ tử của Trường Lưu môn, chàng là chưởng môn Trường Lưu - sư phụ của nàng. Mối tình cấm đoán giữa sư đồ, những hiểu lầm, hy sinh và sự kiên trung trong tình yêu khiến độc giả không khỏi xúc động.",
|
||||
genres: ["ngon-tinh", "tien-hiep"],
|
||||
@@ -71,7 +71,7 @@ export const novels: Novel[] = [
|
||||
id: "4",
|
||||
title: "Anh Hùng Xạ Điêu",
|
||||
slug: "anh-hung-xa-dieu",
|
||||
author: "Kim Dung",
|
||||
authorName: "Kim Dung",
|
||||
coverColor: "from-emerald-500 to-teal-600",
|
||||
description: "Câu chuyện về Quách Tĩnh, một chàng trai chất phác nhưng kiên trì, cùng Hoàng Dung, cô gái thông minh tuyệt đỉnh. Giữa bối cảnh đất nước bị xâm lăng, hai người cùng nhau trải qua bao sóng gió giang hồ, học được những tuyệt kỹ võ công, và cuối cùng trở thành anh hùng dân tộc.",
|
||||
genres: ["kiem-hiep", "lich-su"],
|
||||
@@ -88,7 +88,7 @@ export const novels: Novel[] = [
|
||||
id: "5",
|
||||
title: "Toàn Chức Cao Thủ",
|
||||
slug: "toan-chuc-cao-thu",
|
||||
author: "Hồ Điệp Lam",
|
||||
authorName: "Hồ Điệp Lam",
|
||||
coverColor: "from-cyan-500 to-blue-600",
|
||||
description: "Diệp Tu, đỉnh cao của giới game Glory, bị buộc phải rời đội tuyển chuyên nghiệp. Nhưng với mười năm kinh nghiệm và kỹ thuật vô song, anh bắt đầu lại từ đầu tại một quán net nhỏ. Với tài khoản mới và quyết tâm mãnh liệt, Diệp Tu từng bước quay trở lại đỉnh cao vinh quang.",
|
||||
genres: ["do-thi", "hai-huoc"],
|
||||
@@ -105,7 +105,7 @@ export const novels: Novel[] = [
|
||||
id: "6",
|
||||
title: "Thôn Phệ Tinh Không",
|
||||
slug: "thon-phe-tinh-khong",
|
||||
author: "Thần Đông",
|
||||
authorName: "Thần Đông",
|
||||
coverColor: "from-violet-500 to-purple-600",
|
||||
description: "Trong tương lai, khi Trái Đất trải qua biến cố lớn, con người phát hiện ra năng lực chiến đấu tiềm ẩn. La Phong, một thanh niên bình thường, tình cờ gặp được một sinh vật ngoài hành tinh đặc biệt, từ đó bắt đầu hành trình chinh phục vũ trụ bao la. Từ Trái Đất đến các vì sao, La Phong dần trở thành chiến sĩ mạnh nhất thiên hà.",
|
||||
genres: ["khoa-huyen", "huyen-huyen"],
|
||||
@@ -122,7 +122,7 @@ export const novels: Novel[] = [
|
||||
id: "7",
|
||||
title: "Khánh Dư Niên",
|
||||
slug: "khanh-du-nien",
|
||||
author: "Miêu Nị",
|
||||
authorName: "Miêu Nị",
|
||||
coverColor: "from-yellow-500 to-amber-600",
|
||||
description: "Phạm Nhàn, một thanh niên từ thế giới hiện đại, xuyên không đến một thế giới cổ đại với ký ức về một nền văn minh đã mất. Với kiến thức từ kiếp trước, hắn dần vượt qua các âm mưu cung đình, chiến đấu với các thế lực ngầm, và khám phá ra bí mật kinh thiên về nguồn gốc của thế giới này.",
|
||||
genres: ["lich-su", "huyen-huyen"],
|
||||
@@ -139,7 +139,7 @@ export const novels: Novel[] = [
|
||||
id: "8",
|
||||
title: "Yêu Thần Ký",
|
||||
slug: "yeu-than-ky",
|
||||
author: "Phát Tiêu Đích Mao Nhi",
|
||||
authorName: "Phát Tiêu Đích Mao Nhi",
|
||||
coverColor: "from-red-500 to-rose-600",
|
||||
description: "Nhiếp Ly, vị Yêu Thần hùng mạnh nhất, bị phản bội và hy sinh trong trận chiến cuối cùng. Nhưng khi tỉnh dậy, hắn phát hiện mình đã quay trở lại thời niên thiếu. Với kinh nghiệm và kiến thức từ kiếp trước, Nhiếp Ly quyết tâm thay đổi vận mệnh, cứu lấy những người thân yêu và ngăn chặn thảm họa sắp xảy đến.",
|
||||
genres: ["huyen-huyen", "tien-hiep"],
|
||||
@@ -156,7 +156,7 @@ export const novels: Novel[] = [
|
||||
id: "9",
|
||||
title: "Thiên Quan Tứ Phúc",
|
||||
slug: "thien-quan-tu-phuc",
|
||||
author: "Mặc Hương Đồng Khứu",
|
||||
authorName: "Mặc Hương Đồng Khứu",
|
||||
coverColor: "from-sky-400 to-indigo-500",
|
||||
description: "Tạ Liên, thái tử triều đại Tiên Lạc, ba lần phi thăng thành thiên quan và ba lần bị đánh rơi. Tám trăm năm sau, ngài lại một lần nữa phi thăng, nhưng lần này không ai chào đón. Trong hành trình thu thập công đức, Tạ Liên gặp lại Hoa Thành - một Quỷ vương bí ẩn có mối quan hệ sâu xa với ngài từ tám trăm năm trước.",
|
||||
genres: ["ngon-tinh", "huyen-huyen"],
|
||||
@@ -173,7 +173,7 @@ export const novels: Novel[] = [
|
||||
id: "10",
|
||||
title: "Thám Tử Lừng Danh",
|
||||
slug: "tham-tu-lung-danh",
|
||||
author: "Linh Vũ",
|
||||
authorName: "Linh Vũ",
|
||||
coverColor: "from-slate-500 to-zinc-700",
|
||||
description: "Lâm Phong, một thanh tra cảnh sát trẻ tuổi với khả năng quan sát phi thường, liên tiếp phá giải những vụ án bí ẩn nhất thành phố. Mỗi vụ án đều ẩn chứa những bí mật đen tối, và càng đi sâu, Lâm Phong càng phát hiện ra một tổ chức tội phạm khổng lồ đang ẩn nấp trong bóng tối.",
|
||||
genres: ["trinh-tham", "do-thi"],
|
||||
@@ -190,7 +190,7 @@ export const novels: Novel[] = [
|
||||
id: "11",
|
||||
title: "Đại Quân Sư",
|
||||
slug: "dai-quan-su",
|
||||
author: "Trần Phong",
|
||||
authorName: "Trần Phong",
|
||||
coverColor: "from-green-600 to-emerald-700",
|
||||
description: "Trương Lương, một thiên tài quân sự thời hiện đại, xuyên không về thời Tam Quốc. Với kiến thức chiến thuật vượt thời đại, hắn trở thành quân sư cho một thế lực nhỏ và từng bước thay đổi cục diện thiên hạ. Những trận chiến sử thi, những mưu kế thâm sâu, tất cả đều được tái hiện qua góc nhìn của một người hiện đại.",
|
||||
genres: ["quan-su", "lich-su"],
|
||||
@@ -207,7 +207,7 @@ export const novels: Novel[] = [
|
||||
id: "12",
|
||||
title: "Vạn Giới Thần Chủ",
|
||||
slug: "van-gioi-than-chu",
|
||||
author: "Nhất Niệm Vĩnh Hằng",
|
||||
authorName: "Nhất Niệm Vĩnh Hằng",
|
||||
coverColor: "from-orange-500 to-red-600",
|
||||
description: "Lâm Phàm tình cờ có được một mảnh ngọc bội cổ xưa có thể mở cánh cửa đến vạn giới. Mỗi thế giới đều có quy tắc riêng, sức mạnh riêng, và nguy hiểm riêng. Lâm Phàm phải chinh phục từng thế giới, thu thập sức mạnh và trí tuệ, để cuối cùng trở thành bá chủ vạn giới.",
|
||||
genres: ["tien-hiep", "huyen-huyen"],
|
||||
@@ -355,7 +355,7 @@ export function searchNovels(query: string): Novel[] {
|
||||
return novels.filter(
|
||||
(n) =>
|
||||
n.title.toLowerCase().includes(q) ||
|
||||
n.author.toLowerCase().includes(q) ||
|
||||
n.authorName.toLowerCase().includes(q) ||
|
||||
n.description.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export interface Novel {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
author: string
|
||||
authorName: string
|
||||
coverColor: string
|
||||
description: string
|
||||
genres: string[]
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"epub2": "^3.0.2",
|
||||
"html-to-text": "^9.0.5",
|
||||
"input-otp": "1.4.2",
|
||||
"lucide-react": "^0.564.0",
|
||||
"mongoose": "^9.2.4",
|
||||
@@ -66,6 +68,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
|
||||
Generated
+211
@@ -119,6 +119,12 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0(react@19.2.4)
|
||||
epub2:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(ts-toolbelt@9.6.0)
|
||||
html-to-text:
|
||||
specifier: ^9.0.5
|
||||
version: 9.0.5
|
||||
input-otp:
|
||||
specifier: 1.4.2
|
||||
version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -174,6 +180,9 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
'@types/html-to-text':
|
||||
specifier: ^9.0.4
|
||||
version: 9.0.4
|
||||
'@types/node':
|
||||
specifier: ^22
|
||||
version: 22.19.11
|
||||
@@ -1167,6 +1176,9 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -1289,6 +1301,9 @@ packages:
|
||||
'@types/d3-timer@3.0.2':
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
|
||||
'@types/html-to-text@9.0.4':
|
||||
resolution: {integrity: sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==}
|
||||
|
||||
'@types/node@22.19.11':
|
||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||
|
||||
@@ -1332,10 +1347,17 @@ packages:
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
adm-zip@0.5.16:
|
||||
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
array-hyper-unique@2.1.6:
|
||||
resolution: {integrity: sha512-BdlHRqjKSYs88WFaVNVEc6Kv8ln/FdzCKPbcDPuWs4/EXkQFhnjc8TyR7hnPxRjcjo5LKOhUMGUWpAqRgeJvpA==}
|
||||
|
||||
autoprefixer@10.4.24:
|
||||
resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1347,6 +1369,9 @@ packages:
|
||||
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
|
||||
hasBin: true
|
||||
|
||||
bluebird@3.7.2:
|
||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||
|
||||
browserslist@4.28.1:
|
||||
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
@@ -1379,6 +1404,9 @@ packages:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
crlf-normalize@1.0.20:
|
||||
resolution: {integrity: sha512-h/rBerTd3YHQGfv7tNT25mfhWvRq2BBLCZZ80GFarFxf6HQGbpW6iqDL3N+HBLpjLfAdcBXfWAzVlLfHkRUQBQ==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -1435,6 +1463,14 @@ packages:
|
||||
decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
|
||||
deep-eql@4.0.0:
|
||||
resolution: {integrity: sha512-GxJC5MOg2KyQlv6WiUF/VAnMj4MWnYiXo4oLgeptOELVoknyErb4Z8+5F/IM/K4g9/80YzzatxmWcyRwUseH0A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1445,6 +1481,19 @@ packages:
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
|
||||
domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
dotenv@17.3.1:
|
||||
resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1469,6 +1518,13 @@ packages:
|
||||
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
epub2@3.0.2:
|
||||
resolution: {integrity: sha512-rhvpt27CV5MZfRetfNtdNwi3XcNg1Am0TwfveJkK8YWeHItHepQ8Js9J06v8XRIjuTrCW/NSGYMTy55Of7BfNQ==}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1495,6 +1551,13 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
html-to-text@9.0.5:
|
||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
|
||||
input-otp@1.4.2:
|
||||
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
|
||||
peerDependencies:
|
||||
@@ -1522,6 +1585,9 @@ packages:
|
||||
resolution: {integrity: sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
leac@0.6.0:
|
||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -1734,6 +1800,12 @@ packages:
|
||||
openid-client@5.7.1:
|
||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||
|
||||
parseley@0.12.1:
|
||||
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||
|
||||
@@ -1914,9 +1986,16 @@ packages:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
sax@1.5.0:
|
||||
resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1976,12 +2055,27 @@ packages:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ts-toolbelt@9.6.0:
|
||||
resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==}
|
||||
|
||||
ts-type@3.0.1:
|
||||
resolution: {integrity: sha512-cleRydCkBGBFQ4KAvLH0ARIkciduS745prkGVVxPGvcRGhMMoSJUB7gNR1ByKhFTEYrYRg2CsMRGYnqp+6op+g==}
|
||||
peerDependencies:
|
||||
ts-toolbelt: ^9.6.0
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
tw-animate-css@1.3.3:
|
||||
resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==}
|
||||
|
||||
type-detect@4.1.0:
|
||||
resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
typedarray-dts@1.0.0:
|
||||
resolution: {integrity: sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==}
|
||||
|
||||
typescript@5.7.3:
|
||||
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -2042,6 +2136,14 @@ packages:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
xml2js@0.6.2:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xmlbuilder@11.0.1:
|
||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
@@ -2971,6 +3073,11 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -3068,6 +3175,8 @@ snapshots:
|
||||
|
||||
'@types/d3-timer@3.0.2': {}
|
||||
|
||||
'@types/html-to-text@9.0.4': {}
|
||||
|
||||
'@types/node@22.19.11':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -3091,10 +3200,17 @@ snapshots:
|
||||
next: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
|
||||
adm-zip@0.5.16: {}
|
||||
|
||||
aria-hidden@1.2.6:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
array-hyper-unique@2.1.6:
|
||||
dependencies:
|
||||
deep-eql: 4.0.0
|
||||
lodash: 4.17.23
|
||||
|
||||
autoprefixer@10.4.24(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -3106,6 +3222,8 @@ snapshots:
|
||||
|
||||
baseline-browser-mapping@2.9.19: {}
|
||||
|
||||
bluebird@3.7.2: {}
|
||||
|
||||
browserslist@4.28.1:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.9.19
|
||||
@@ -3140,6 +3258,12 @@ snapshots:
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
crlf-normalize@1.0.20(ts-toolbelt@9.6.0):
|
||||
dependencies:
|
||||
ts-type: 3.0.1(ts-toolbelt@9.6.0)
|
||||
transitivePeerDependencies:
|
||||
- ts-toolbelt
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
@@ -3186,6 +3310,12 @@ snapshots:
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
deep-eql@4.0.0:
|
||||
dependencies:
|
||||
type-detect: 4.1.0
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
@@ -3195,6 +3325,24 @@ snapshots:
|
||||
'@babel/runtime': 7.28.6
|
||||
csstype: 3.2.3
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
|
||||
domhandler@5.0.3:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
domutils@3.2.2:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
dotenv@17.3.1: {}
|
||||
|
||||
electron-to-chromium@1.5.286: {}
|
||||
@@ -3216,6 +3364,19 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.0
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
epub2@3.0.2(ts-toolbelt@9.6.0):
|
||||
dependencies:
|
||||
adm-zip: 0.5.16
|
||||
array-hyper-unique: 2.1.6
|
||||
bluebird: 3.7.2
|
||||
crlf-normalize: 1.0.20(ts-toolbelt@9.6.0)
|
||||
tslib: 2.8.1
|
||||
xml2js: 0.6.2
|
||||
transitivePeerDependencies:
|
||||
- ts-toolbelt
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
@@ -3231,6 +3392,21 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
html-to-text@9.0.5:
|
||||
dependencies:
|
||||
'@selderee/plugin-htmlparser2': 0.11.0
|
||||
deepmerge: 4.3.1
|
||||
dom-serializer: 2.0.0
|
||||
htmlparser2: 8.0.2
|
||||
selderee: 0.11.0
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.2.2
|
||||
entities: 4.5.0
|
||||
|
||||
input-otp@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
@@ -3248,6 +3424,8 @@ snapshots:
|
||||
|
||||
kareem@3.2.0: {}
|
||||
|
||||
leac@0.6.0: {}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
optional: true
|
||||
|
||||
@@ -3416,6 +3594,13 @@ snapshots:
|
||||
object-hash: 2.2.0
|
||||
oidc-token-hash: 5.2.0
|
||||
|
||||
parseley@0.12.1:
|
||||
dependencies:
|
||||
leac: 0.6.0
|
||||
peberminta: 0.9.0
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
pg-cloudflare@1.3.0:
|
||||
optional: true
|
||||
|
||||
@@ -3594,8 +3779,14 @@ snapshots:
|
||||
tiny-invariant: 1.3.3
|
||||
victory-vendor: 36.9.2
|
||||
|
||||
sax@1.5.0: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
|
||||
semver@7.7.4:
|
||||
optional: true
|
||||
|
||||
@@ -3663,10 +3854,23 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
ts-toolbelt@9.6.0: {}
|
||||
|
||||
ts-type@3.0.1(ts-toolbelt@9.6.0):
|
||||
dependencies:
|
||||
'@types/node': 22.19.11
|
||||
ts-toolbelt: 9.6.0
|
||||
tslib: 2.8.1
|
||||
typedarray-dts: 1.0.0
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.3.3: {}
|
||||
|
||||
type-detect@4.1.0: {}
|
||||
|
||||
typedarray-dts@1.0.0: {}
|
||||
|
||||
typescript@5.7.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
@@ -3731,6 +3935,13 @@ snapshots:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
xml2js@0.6.2:
|
||||
dependencies:
|
||||
sax: 1.5.0
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xmlbuilder@11.0.1: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user