Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
fevirtus/reader:latest
|
||||||
|
ghcr.io/fevirtus/reader:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -11,6 +11,8 @@ import { convert } from "html-to-text"
|
|||||||
import { slugify } from "@/lib/utils"
|
import { slugify } from "@/lib/utils"
|
||||||
import { deleteR2ObjectByUrl, uploadBufferToR2 } from "@/lib/r2"
|
import { deleteR2ObjectByUrl, uploadBufferToR2 } from "@/lib/r2"
|
||||||
|
|
||||||
|
export const maxDuration = 900
|
||||||
|
|
||||||
type SplitMode = "toc" | "regex"
|
type SplitMode = "toc" | "regex"
|
||||||
type SeriesMode = "none" | "existing" | "new"
|
type SeriesMode = "none" | "existing" | "new"
|
||||||
|
|
||||||
@@ -38,6 +40,81 @@ interface EpubCoverAsset {
|
|||||||
sourceId: string | null
|
sourceId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EpubCtor = new (epubfile: string, imagewebroot: string, chapterwebroot: string) => any
|
||||||
|
|
||||||
|
function sanitizeEpubNavMapBranch(branch: any): any {
|
||||||
|
if (Array.isArray(branch)) {
|
||||||
|
branch.forEach((item) => sanitizeEpubNavMapBranch(item))
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!branch || typeof branch !== "object") {
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("navLabel" in branch) {
|
||||||
|
const navLabel = (branch as any).navLabel
|
||||||
|
|
||||||
|
if (typeof navLabel === "string") {
|
||||||
|
;(branch as any).navLabel = navLabel
|
||||||
|
} else if (navLabel && typeof navLabel === "object") {
|
||||||
|
if (typeof navLabel.text === "string") {
|
||||||
|
;(branch as any).navLabel = navLabel.text
|
||||||
|
} else if (navLabel.text !== undefined && navLabel.text !== null) {
|
||||||
|
;(branch as any).navLabel = String(navLabel.text)
|
||||||
|
} else {
|
||||||
|
;(branch as any).navLabel = ""
|
||||||
|
}
|
||||||
|
} else if (navLabel === null || navLabel === undefined) {
|
||||||
|
;(branch as any).navLabel = ""
|
||||||
|
} else {
|
||||||
|
;(branch as any).navLabel = String(navLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((branch as any).navPoint) {
|
||||||
|
sanitizeEpubNavMapBranch((branch as any).navPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPatchedEpubCtor(): EpubCtor {
|
||||||
|
const loaded = require("epub2")
|
||||||
|
const EPub = (loaded?.EPub || loaded) as EpubCtor
|
||||||
|
const proto = (EPub as any)?.prototype
|
||||||
|
|
||||||
|
if (proto && !proto.__readerSafeNavLabelPatchApplied && typeof proto.walkNavMap === "function") {
|
||||||
|
const originalWalkNavMap = proto.walkNavMap
|
||||||
|
|
||||||
|
proto.walkNavMap = function patchedWalkNavMap(branch: any, ...args: any[]) {
|
||||||
|
const safeBranch = sanitizeEpubNavMapBranch(branch)
|
||||||
|
return originalWalkNavMap.call(this, safeBranch, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.__readerSafeNavLabelPatchApplied = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return EPub
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRequestAbortedError(error: unknown): boolean {
|
||||||
|
if (!error) return false
|
||||||
|
|
||||||
|
const candidate = error as { code?: unknown; message?: unknown; name?: unknown }
|
||||||
|
const code = typeof candidate.code === "string" ? candidate.code.toUpperCase() : ""
|
||||||
|
const message = typeof candidate.message === "string" ? candidate.message.toLowerCase() : ""
|
||||||
|
const name = typeof candidate.name === "string" ? candidate.name.toLowerCase() : ""
|
||||||
|
|
||||||
|
return (
|
||||||
|
code === "ECONNRESET" ||
|
||||||
|
code === "ABORT_ERR" ||
|
||||||
|
message.includes("aborted") ||
|
||||||
|
message.includes("connection reset") ||
|
||||||
|
name.includes("abort")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CHAPTER_REGEX_PRESETS: Record<string, string> = {
|
const CHAPTER_REGEX_PRESETS: Record<string, string> = {
|
||||||
vi_chuong: "^(?:Chương|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
|
vi_chuong: "^(?:Chương|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
|
||||||
en_chapter: "^(?:Chapter|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
|
en_chapter: "^(?:Chapter|Ch\\.)\\s*\\d+(?:\\.\\d+)?[^\\n]*$",
|
||||||
@@ -838,7 +915,7 @@ async function saveCoverBufferToR2(cover: EpubCoverAsset): Promise<string | null
|
|||||||
|
|
||||||
async function parseEpubSections(tempFilePath: string): Promise<{ metadata: any; sections: EpubSection[]; cover: EpubCoverAsset }> {
|
async function parseEpubSections(tempFilePath: string): Promise<{ metadata: any; sections: EpubSection[]; cover: EpubCoverAsset }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const EPub = require("epub2").EPub || require("epub2")
|
const EPub = getPatchedEpubCtor()
|
||||||
const epub = new EPub(tempFilePath, "", "")
|
const epub = new EPub(tempFilePath, "", "")
|
||||||
|
|
||||||
epub.on("error", (err: any) => reject(err))
|
epub.on("error", (err: any) => reject(err))
|
||||||
@@ -1168,6 +1245,11 @@ export async function POST(req: Request) {
|
|||||||
replaced,
|
replaced,
|
||||||
}, { status: responseStatus })
|
}, { status: responseStatus })
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (isRequestAbortedError(error)) {
|
||||||
|
console.warn("EPUB upload aborted by client or network interruption")
|
||||||
|
return NextResponse.json({ error: "Kết nối upload bị ngắt trong lúc xử lý" }, { status: 499 })
|
||||||
|
}
|
||||||
|
|
||||||
console.error("EPUB upload error:", error)
|
console.error("EPUB upload error:", error)
|
||||||
return NextResponse.json({ error: "Lỗi xử lý file EPUB", details: error.message }, { status: 500 })
|
return NextResponse.json({ error: "Lỗi xử lý file EPUB", details: error.message }, { status: 500 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,28 @@ function toUTCDateOnly(value: Date): Date {
|
|||||||
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()))
|
return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function upsertDailyNovelView(novelId: string, day: Date) {
|
||||||
|
const delegate = (prisma as any).novelViewDaily
|
||||||
|
if (!delegate || typeof delegate.upsert !== "function") return
|
||||||
|
|
||||||
|
await delegate.upsert({
|
||||||
|
where: {
|
||||||
|
novelId_day: {
|
||||||
|
novelId,
|
||||||
|
day,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
views: { increment: 1 },
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
novelId,
|
||||||
|
day,
|
||||||
|
views: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Lấy danh sách bookmark
|
// Lấy danh sách bookmark
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -140,28 +162,11 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
if (shouldIncrementNovelView) {
|
if (shouldIncrementNovelView) {
|
||||||
const day = toUTCDateOnly(new Date())
|
const day = toUTCDateOnly(new Date())
|
||||||
await prisma.$transaction([
|
await prisma.novel.update({
|
||||||
prisma.novel.update({
|
where: { id: novelId },
|
||||||
where: { id: novelId },
|
data: { views: { increment: 1 } }
|
||||||
data: { views: { increment: 1 } }
|
})
|
||||||
}),
|
await upsertDailyNovelView(novelId, day)
|
||||||
prisma.novelViewDaily.upsert({
|
|
||||||
where: {
|
|
||||||
novelId_day: {
|
|
||||||
novelId,
|
|
||||||
day,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
views: { increment: 1 },
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
novelId,
|
|
||||||
day,
|
|
||||||
views: 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ status: "updated", bookmark })
|
return NextResponse.json({ status: "updated", bookmark })
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ export function NovelClient() {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest()
|
const xhr = new XMLHttpRequest()
|
||||||
xhr.open("POST", "/api/mod/epub")
|
xhr.open("POST", "/api/mod/epub")
|
||||||
|
// Large EPUB imports can take several minutes when parsing + writing many chapters.
|
||||||
|
xhr.timeout = 15 * 60 * 1000
|
||||||
|
|
||||||
xhr.upload.onprogress = (event) => {
|
xhr.upload.onprogress = (event) => {
|
||||||
if (!onProgress || !event.lengthComputable) return
|
if (!onProgress || !event.lengthComputable) return
|
||||||
@@ -232,6 +234,8 @@ export function NovelClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xhr.onerror = () => reject(new Error("Không thể kết nối tới server"))
|
xhr.onerror = () => reject(new Error("Không thể kết nối tới server"))
|
||||||
|
xhr.onabort = () => reject(new Error("Upload đã bị huỷ hoặc kết nối bị ngắt"))
|
||||||
|
xhr.ontimeout = () => reject(new Error("Upload quá lâu và đã hết thời gian chờ"))
|
||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
let data: EpubUploadResponseData = {}
|
let data: EpubUploadResponseData = {}
|
||||||
@@ -947,79 +951,89 @@ export function NovelClient() {
|
|||||||
try {
|
try {
|
||||||
for (const file of pendingEpubFiles) {
|
for (const file of pendingEpubFiles) {
|
||||||
const fileKey = buildEpubFileKey(file)
|
const fileKey = buildEpubFileKey(file)
|
||||||
const formData = new FormData()
|
try {
|
||||||
formData.append("file", file)
|
const formData = new FormData()
|
||||||
formData.append("seriesMode", epubSeriesMode)
|
formData.append("file", file)
|
||||||
if (epubSeriesMode === "existing") formData.append("seriesId", epubSeriesId)
|
formData.append("seriesMode", epubSeriesMode)
|
||||||
if (epubSeriesMode === "new") formData.append("seriesName", epubSeriesName.trim())
|
if (epubSeriesMode === "existing") formData.append("seriesId", epubSeriesId)
|
||||||
formData.append("splitMode", "toc")
|
if (epubSeriesMode === "new") formData.append("seriesName", epubSeriesName.trim())
|
||||||
|
formData.append("splitMode", "toc")
|
||||||
|
|
||||||
setBulkProgressItem(fileKey, {
|
setBulkProgressItem(fileKey, {
|
||||||
status: "uploading",
|
status: "uploading",
|
||||||
progress: 1,
|
progress: 1,
|
||||||
message: "Đang upload...",
|
message: "Đang upload...",
|
||||||
})
|
})
|
||||||
|
|
||||||
let upload = await uploadEpubRequest(formData, (progress) => {
|
let upload = await uploadEpubRequest(formData, (progress) => {
|
||||||
setBulkProgressItem(fileKey, { progress, status: "uploading" })
|
setBulkProgressItem(fileKey, { progress, status: "uploading" })
|
||||||
})
|
})
|
||||||
|
|
||||||
if (upload.status === 409 && upload.data?.code === "DUPLICATE_TITLE") {
|
if (upload.status === 409 && upload.data?.code === "DUPLICATE_TITLE") {
|
||||||
const duplicateTitle = upload.data.existingNovel?.title || file.name
|
const duplicateTitle = upload.data.existingNovel?.title || file.name
|
||||||
|
|
||||||
if (upload.data.canReplace === false) {
|
if (upload.data.canReplace === false) {
|
||||||
|
failed += 1
|
||||||
|
setBulkProgressItem(fileKey, {
|
||||||
|
status: "failed",
|
||||||
|
progress: 100,
|
||||||
|
message: upload.data.error || `Trùng tên ${duplicateTitle} nhưng không đủ quyền replace`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldReplace = false
|
||||||
|
if (bulkDuplicateHandling === "replace-all") {
|
||||||
|
shouldReplace = true
|
||||||
|
} else if (bulkDuplicateHandling === "skip-all") {
|
||||||
|
shouldReplace = false
|
||||||
|
} else {
|
||||||
|
shouldReplace = window.confirm(`File ${file.name} trùng với truyện "${duplicateTitle}". Bạn có muốn replace không?`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldReplace) {
|
||||||
|
skipped += 1
|
||||||
|
setBulkProgressItem(fileKey, {
|
||||||
|
status: "skipped",
|
||||||
|
progress: 100,
|
||||||
|
message: bulkDuplicateHandling === "skip-all" ? "Bỏ qua theo cấu hình" : "Đã bỏ qua do trùng tên",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryFormData = cloneFormData(formData)
|
||||||
|
retryFormData.set("replaceExisting", "true")
|
||||||
|
upload = await uploadEpubRequest(retryFormData, (progress) => {
|
||||||
|
setBulkProgressItem(fileKey, { progress, status: "uploading" })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upload.ok) {
|
||||||
|
success += 1
|
||||||
|
if (upload.data?.replaced) {
|
||||||
|
replaced += 1
|
||||||
|
}
|
||||||
|
setBulkProgressItem(fileKey, {
|
||||||
|
status: "success",
|
||||||
|
progress: 100,
|
||||||
|
message: upload.data?.replaced ? "Đã replace thành công" : "Upload thành công",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
failed += 1
|
failed += 1
|
||||||
setBulkProgressItem(fileKey, {
|
setBulkProgressItem(fileKey, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
progress: 100,
|
progress: 100,
|
||||||
message: upload.data.error || `Trùng tên ${duplicateTitle} nhưng không đủ quyền replace`,
|
message: upload.data?.error || "Upload thất bại",
|
||||||
})
|
})
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
let shouldReplace = false
|
|
||||||
if (bulkDuplicateHandling === "replace-all") {
|
|
||||||
shouldReplace = true
|
|
||||||
} else if (bulkDuplicateHandling === "skip-all") {
|
|
||||||
shouldReplace = false
|
|
||||||
} else {
|
|
||||||
shouldReplace = window.confirm(`File ${file.name} trùng với truyện "${duplicateTitle}". Bạn có muốn replace không?`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldReplace) {
|
|
||||||
skipped += 1
|
|
||||||
setBulkProgressItem(fileKey, {
|
|
||||||
status: "skipped",
|
|
||||||
progress: 100,
|
|
||||||
message: bulkDuplicateHandling === "skip-all" ? "Bỏ qua theo cấu hình" : "Đã bỏ qua do trùng tên",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryFormData = cloneFormData(formData)
|
|
||||||
retryFormData.set("replaceExisting", "true")
|
|
||||||
upload = await uploadEpubRequest(retryFormData, (progress) => {
|
|
||||||
setBulkProgressItem(fileKey, { progress, status: "uploading" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (upload.ok) {
|
|
||||||
success += 1
|
|
||||||
if (upload.data?.replaced) {
|
|
||||||
replaced += 1
|
|
||||||
}
|
|
||||||
setBulkProgressItem(fileKey, {
|
|
||||||
status: "success",
|
|
||||||
progress: 100,
|
|
||||||
message: upload.data?.replaced ? "Đã replace thành công" : "Upload thành công",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
failed += 1
|
failed += 1
|
||||||
setBulkProgressItem(fileKey, {
|
setBulkProgressItem(fileKey, {
|
||||||
status: "failed",
|
status: "failed",
|
||||||
progress: 100,
|
progress: 100,
|
||||||
message: upload.data?.error || "Upload thất bại",
|
message: err?.message || "Upload thất bại do lỗi kết nối",
|
||||||
})
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -348,8 +348,13 @@ export default async function HomePage() {
|
|||||||
|
|
||||||
const hotWeekly = weeklyRanking.slice(0, 5).map((entry) => ({ ...entry, source: "week" as const }))
|
const hotWeekly = weeklyRanking.slice(0, 5).map((entry) => ({ ...entry, source: "week" as const }))
|
||||||
const hotMonthly = monthlyRanking.slice(0, 5).map((entry) => ({ ...entry, source: "month" as const }))
|
const hotMonthly = monthlyRanking.slice(0, 5).map((entry) => ({ ...entry, source: "month" as const }))
|
||||||
|
const hotAllTime = allTimeRanking.slice(0, 8).map((entry) => ({ ...entry, source: "all" as const }))
|
||||||
|
|
||||||
hotSlides = toHotCarouselItems([...hotWeekly, ...hotMonthly]).slice(0, 10)
|
hotSlides = fillUniqueRows(
|
||||||
|
toHotCarouselItems([...hotWeekly, ...hotMonthly]),
|
||||||
|
toHotCarouselItems(hotAllTime),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
const usedHotIds = new Set(hotSlides.map((item) => item.id))
|
const usedHotIds = new Set(hotSlides.map((item) => item.id))
|
||||||
const randomPool = randomPoolRaw as HomeNovel[]
|
const randomPool = randomPoolRaw as HomeNovel[]
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export function HomeHotCarousel({ items }: { items: HotCarouselItem[] }) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-border/70 bg-card/90 shadow-[0_20px_60px_-40px_rgba(251,146,60,0.45)]">
|
<div className="relative overflow-hidden rounded-2xl border border-border/70 bg-card/90 shadow-[0_20px_60px_-40px_rgba(251,146,60,0.45)]">
|
||||||
<div className="flex transition-transform duration-500" style={{ transform: `translateX(-${activeIndex * 100}%)` }}>
|
<div className="flex transition-transform duration-500" style={{ transform: `translateX(-${activeIndex * 100}%)` }}>
|
||||||
{items.map((item) => (
|
{items.map((item, index) => (
|
||||||
<div key={item.id} className="min-w-full">
|
<div key={`${item.id}-${item.hotSource}-${index}`} className="min-w-full">
|
||||||
<Link href={`/truyen/${item.slug}`} className="group block">
|
<Link href={`/truyen/${item.slug}`} className="group block">
|
||||||
<div className="grid gap-0 md:grid-cols-[320px_1fr]">
|
<div className="grid gap-0 md:grid-cols-[320px_1fr]">
|
||||||
<div className="relative h-[420px] overflow-hidden bg-muted/60 md:h-[460px]">
|
<div className="relative h-[420px] overflow-hidden bg-muted/60 md:h-[460px]">
|
||||||
@@ -137,7 +137,7 @@ export function HomeHotCarousel({ items }: { items: HotCarouselItem[] }) {
|
|||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={`${item.id}-${item.hotSource}-dot-${index}`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Slide ${index + 1}`}
|
aria-label={`Slide ${index + 1}`}
|
||||||
onClick={() => setActiveIndex(index)}
|
onClick={() => setActiveIndex(index)}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import "./.next/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@prisma/client': false
|
||||||
|
'@prisma/engines': false
|
||||||
|
prisma: false
|
||||||
|
sharp: false
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user