Refactor code structure for improved readability and maintainability
This commit is contained in:
+40
-24
@@ -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)
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user