Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-03-23 11:12:56 +07:00
parent e345d9ccce
commit ffd177718f
39 changed files with 5258 additions and 520 deletions
+40 -24
View File
@@ -7,8 +7,8 @@ import { useAuth } from "./auth-context"
interface BookmarkContextType {
bookmarks: Bookmark[]
isBookmarked: (novelId: string) => boolean
toggleBookmark: (novelId: string) => void
updateProgress: (novelId: string, chapterId: string, chapterNumber: number) => void
toggleBookmark: (novelId: string) => Promise<void>
updateProgress: (novelId: string, chapterId: string, chapterNumber: number) => Promise<void>
getProgress: (novelId: string) => Bookmark | undefined
}
@@ -18,27 +18,27 @@ export function BookmarkProvider({ children }: { children: ReactNode }) {
const { user } = useAuth()
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
useEffect(() => {
let mounted = true
const fetchBookmarks = async () => {
if (!user) {
setBookmarks([])
return
}
try {
const res = await fetch("/api/user/bookmarks")
if (res.ok) {
const data = await res.json()
if (mounted) setBookmarks(data)
}
} catch (e) {
console.error("Failed to fetch bookmarks", e)
}
const fetchBookmarks = useCallback(async () => {
if (!user) {
setBookmarks([])
return
}
try {
const res = await fetch("/api/user/bookmarks")
if (!res.ok) return
const data = await res.json()
setBookmarks(Array.isArray(data) ? data : [])
} catch (e) {
console.error("Failed to fetch bookmarks", e)
}
fetchBookmarks()
return () => { mounted = false }
}, [user])
useEffect(() => {
fetchBookmarks()
}, [fetchBookmarks])
const toggleBookmark = useCallback(async (novelId: string) => {
if (!user) return
@@ -52,14 +52,22 @@ export function BookmarkProvider({ children }: { children: ReactNode }) {
})
try {
await fetch("/api/user/bookmarks", {
const res = await fetch("/api/user/bookmarks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "toggle", novelId })
})
if (!res.ok) {
throw new Error("Không thể cập nhật đánh dấu")
}
await fetchBookmarks()
} catch (e) {
console.error(e)
await fetchBookmarks()
}
}, [user])
}, [fetchBookmarks, user])
const updateProgress = useCallback(async (novelId: string, chapterId: string, chapterNumber: number) => {
if (!user) return
@@ -74,14 +82,22 @@ export function BookmarkProvider({ children }: { children: ReactNode }) {
})
try {
await fetch("/api/user/bookmarks", {
const res = await fetch("/api/user/bookmarks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "updateProgress", novelId, lastChapterId: chapterId, lastChapterNumber: chapterNumber })
})
if (!res.ok) {
throw new Error("Không thể cập nhật tiến độ")
}
await fetchBookmarks()
} catch (e) {
console.error(e)
await fetchBookmarks()
}
}, [user])
}, [fetchBookmarks, user])
const getProgress = useCallback((novelId: string) => {
return bookmarks.find((b) => b.novelId === novelId)
+25
View File
@@ -0,0 +1,25 @@
export const MOD_AI_PREFILL_STORAGE_KEY = "mod:ai-tool:novel-prefill"
export const MOD_AI_MODEL_STORAGE_KEY = "mod:ai-tool:model"
export const MOD_AI_WEB_DEFAULT_MODEL = "gpt-4o-mini-search-preview"
export const MOD_AI_WEB_MODEL_OPTIONS = [
{
value: "gpt-4o-mini-search-preview",
label: "gpt-4o-mini-search-preview (nhanh)",
},
{
value: "gpt-4o-search-preview",
label: "gpt-4o-search-preview (chat luong cao)",
},
] as const
export type AINovelPrefillPayload = {
title?: string
originalTitle?: string
authorName?: string
originalAuthorName?: string
description?: string
coverUrl?: string
status?: "Đang ra" | "Hoàn thành" | "Tạm ngưng"
genresSuggested?: string[]
}
+1
View File
@@ -25,5 +25,6 @@ const ChapterSchema: Schema = new Schema({
})
ChapterSchema.index({ novelId: 1, number: 1 }, { unique: true })
ChapterSchema.index({ createdAt: -1, novelId: 1 })
export const Chapter = mongoose.models.Chapter || mongoose.model<IChapter>("Chapter", ChapterSchema)
+25
View File
@@ -0,0 +1,25 @@
import mongoose, { Schema, Document } from "mongoose"
export interface IEditorRecommendation extends Document {
novelId: string
editorId: string
createdAt: Date
updatedAt: Date
}
const EditorRecommendationSchema: Schema = new Schema(
{
novelId: { type: String, required: true, index: true },
editorId: { type: String, required: true, index: true },
},
{
timestamps: true,
}
)
EditorRecommendationSchema.index({ novelId: 1, editorId: 1 }, { unique: true })
EditorRecommendationSchema.index({ createdAt: -1 })
export const EditorRecommendation =
mongoose.models.EditorRecommendation ||
mongoose.model<IEditorRecommendation>("EditorRecommendation", EditorRecommendationSchema)
+25
View File
@@ -0,0 +1,25 @@
import mongoose, { Document, Schema } from "mongoose"
export interface IUserRecommendation extends Document {
userId: string
novelId: string
createdAt: Date
updatedAt: Date
}
const UserRecommendationSchema: Schema = new Schema(
{
userId: { type: String, required: true, index: true },
novelId: { type: String, required: true, index: true },
},
{
timestamps: true,
}
)
UserRecommendationSchema.index({ userId: 1, novelId: 1 }, { unique: true })
UserRecommendationSchema.index({ createdAt: -1 })
export const UserRecommendation =
mongoose.models.UserRecommendation ||
mongoose.model<IUserRecommendation>("UserRecommendation", UserRecommendationSchema)
+125
View File
@@ -0,0 +1,125 @@
"use client"
import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from "react"
import { useAuth } from "@/lib/auth-context"
type UserRecommendedNovel = {
id: string
title: string
slug: string
authorName: string
coverUrl: string | null
status: string
totalChapters: number
}
type UserRecommendationItem = {
id: string
novelId: string
createdAt: string | null
novel: UserRecommendedNovel
}
type RecommendationContextType = {
recommendations: UserRecommendationItem[]
isRecommended: (novelId: string) => boolean
toggleRecommendation: (novelId: string) => Promise<{ status: "added" | "removed" | "exists" }>
}
const RecommendationContext = createContext<RecommendationContextType | undefined>(undefined)
export function RecommendationProvider({ children }: { children: ReactNode }) {
const { user } = useAuth()
const [recommendations, setRecommendations] = useState<UserRecommendationItem[]>([])
const fetchRecommendations = useCallback(async () => {
if (!user) {
setRecommendations([])
return
}
try {
const res = await fetch("/api/user/recommendations")
if (!res.ok) {
setRecommendations([])
return
}
const data = await res.json()
setRecommendations(Array.isArray(data) ? data : [])
} catch (error) {
console.error("Failed to fetch recommendations", error)
}
}, [user])
useEffect(() => {
fetchRecommendations()
}, [fetchRecommendations])
const isRecommended = useCallback(
(novelId: string) => recommendations.some((item) => item.novelId === novelId),
[recommendations]
)
const toggleRecommendation = useCallback(
async (novelId: string) => {
if (!user) throw new Error("Unauthorized")
if (!novelId) throw new Error("Missing novel id")
const existed = recommendations.some((item) => item.novelId === novelId)
if (existed) {
const res = await fetch(`/api/user/recommendations?novelId=${encodeURIComponent(novelId)}`, {
method: "DELETE",
})
const data = (await res.json()) as { error?: string }
if (!res.ok) {
throw new Error(data.error || "Không thể bỏ đề cử")
}
await fetchRecommendations()
return { status: "removed" as const }
}
const res = await fetch("/api/user/recommendations", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ novelId }),
})
const data = (await res.json()) as { error?: string }
if (!res.ok) {
if (res.status === 409) {
await fetchRecommendations()
return { status: "exists" as const }
}
throw new Error(data.error || "Không thể đề cử truyện")
}
await fetchRecommendations()
return { status: "added" as const }
},
[fetchRecommendations, recommendations, user]
)
return (
<RecommendationContext.Provider
value={{
recommendations,
isRecommended,
toggleRecommendation,
}}
>
{children}
</RecommendationContext.Provider>
)
}
export function useRecommendations() {
const context = useContext(RecommendationContext)
if (!context) throw new Error("useRecommendations must be used within RecommendationProvider")
return context
}