mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 15:07:46 +00:00
Add database migration snapshots for comment_votes, comments, movies, and users tables
- Created 0009_snapshot.json with initial structure for comment_votes, comments, movies, and users tables. - Created 0010_snapshot.json with updated structure for comments, including new fields: pinned and spoiler. - Created 0011_snapshot.json with further updates to comments, adding anonymous field and refining existing structures.
This commit is contained in:
@@ -0,0 +1,364 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CornerDownLeft, Crown, Loader2, MessageSquare, Send, ThumbsDown, ThumbsUp, Trash2, User } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
source: string
|
||||||
|
slug: string
|
||||||
|
movieName?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { user } = useAuth()
|
||||||
|
const { fetchComments, postComment, deleteComment, voteComment } = useComments()
|
||||||
|
const { getDraft, saveDraft, deleteDraft } = useCommentDraft()
|
||||||
|
|
||||||
|
const commentContent = ref('')
|
||||||
|
const isCommentSubmitting = ref(false)
|
||||||
|
const hasDraft = ref(false)
|
||||||
|
const isAnonymous = ref(false)
|
||||||
|
let draftAutoSaveTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
const comments = ref<any[]>([])
|
||||||
|
const isCommentsLoading = ref(false)
|
||||||
|
const replyContent = ref<Record<number, string>>({})
|
||||||
|
const isReplyingTo = ref<number | null>(null)
|
||||||
|
const expandedComments = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
async function loadComments() {
|
||||||
|
if (!props.slug) return
|
||||||
|
isCommentsLoading.value = true
|
||||||
|
try {
|
||||||
|
comments.value = await fetchComments(props.source, props.slug, user.value?.id)
|
||||||
|
} finally {
|
||||||
|
isCommentsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCommentDraft() {
|
||||||
|
const draft = getDraft(props.slug, props.source)
|
||||||
|
if (draft) {
|
||||||
|
commentContent.value = draft.content
|
||||||
|
hasDraft.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerAutoSaveDraft() {
|
||||||
|
if (draftAutoSaveTimer) clearTimeout(draftAutoSaveTimer)
|
||||||
|
draftAutoSaveTimer = setTimeout(() => {
|
||||||
|
if (!commentContent.value.trim()) return
|
||||||
|
saveDraft(props.slug, props.source, commentContent.value.trim(), props.movieName)
|
||||||
|
hasDraft.value = true
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteDraft() {
|
||||||
|
saveDraft(props.slug, props.source, '', props.movieName)
|
||||||
|
deleteDraft(props.slug, props.source)
|
||||||
|
commentContent.value = ''
|
||||||
|
hasDraft.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmitComment() {
|
||||||
|
if (!commentContent.value.trim() || isCommentSubmitting.value || !user.value) return
|
||||||
|
isCommentSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await postComment(props.source, props.slug, commentContent.value.trim(), props.movieName, undefined, false, isAnonymous.value)
|
||||||
|
deleteDraft(props.slug, props.source)
|
||||||
|
commentContent.value = ''
|
||||||
|
hasDraft.value = false
|
||||||
|
isAnonymous.value = false
|
||||||
|
await loadComments()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Lỗi:', e)
|
||||||
|
} finally {
|
||||||
|
isCommentSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteComment(id: number) {
|
||||||
|
try {
|
||||||
|
await deleteComment(id)
|
||||||
|
await loadComments()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Lỗi:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startReply(commentId: number) {
|
||||||
|
if (!user.value) return
|
||||||
|
isReplyingTo.value = commentId
|
||||||
|
replyContent.value[commentId] = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelReply() {
|
||||||
|
isReplyingTo.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReply(commentId: number) {
|
||||||
|
const content = replyContent.value[commentId]?.trim()
|
||||||
|
if (!content || !user.value) return
|
||||||
|
try {
|
||||||
|
await postComment(props.source, props.slug, content, props.movieName, commentId, false, isAnonymous.value)
|
||||||
|
replyContent.value[commentId] = ''
|
||||||
|
isReplyingTo.value = null
|
||||||
|
isAnonymous.value = false
|
||||||
|
await loadComments()
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Lỗi:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVote(commentId: number, vote: number, isReply = false, parentId?: number) {
|
||||||
|
if (!user.value) return
|
||||||
|
try {
|
||||||
|
const result = await voteComment(commentId, vote) as any
|
||||||
|
if (isReply && parentId) {
|
||||||
|
const comment = comments.value.find((c: any) => c.id === parentId)
|
||||||
|
const reply = comment?.replies?.find((r: any) => r.id === commentId)
|
||||||
|
if (reply) {
|
||||||
|
reply.userVote = result.vote
|
||||||
|
reply.likeCount = result.likeCount
|
||||||
|
reply.dislikeCount = result.dislikeCount
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const comment = comments.value.find((c: any) => c.id === commentId)
|
||||||
|
if (comment) {
|
||||||
|
comment.userVote = result.vote
|
||||||
|
comment.likeCount = result.likeCount
|
||||||
|
comment.dislikeCount = result.dislikeCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error('Lỗi')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(commentId: number) {
|
||||||
|
const s = new Set(expandedComments.value)
|
||||||
|
if (s.has(commentId)) s.delete(commentId)
|
||||||
|
else s.add(commentId)
|
||||||
|
expandedComments.value = s
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(dateStr: string): string {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const now = new Date()
|
||||||
|
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||||
|
if (seconds < 60) return 'Vừa xong'
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
if (minutes < 60) return `${minutes} phút trước`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
if (hours < 24) return `${hours} giờ trước`
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
if (days < 30) return `${days} ngày trước`
|
||||||
|
return date.toLocaleDateString('vi-VN')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCommentDraft()
|
||||||
|
loadComments()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.slug, () => {
|
||||||
|
loadCommentDraft()
|
||||||
|
loadComments()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-10 border-t border-white/10 pt-8">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<MessageSquare class="size-5 text-yellow-300" />
|
||||||
|
<h2 class="text-xl font-bold text-white">Bình luận</h2>
|
||||||
|
<span class="text-sm text-slate-400">({{ comments.length }})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!user" class="flex items-center gap-3 mb-8 p-4 bg-[#13151f] border border-white/5 rounded-xl">
|
||||||
|
<p class="text-sm text-slate-400">Đăng nhập để bình luận về phim này.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="mb-8 p-4 rounded-xl bg-[#13151f] border border-white/5">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="size-9 rounded-full bg-white/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
|
<img v-if="user?.avatar" :src="user.avatar" class="size-9 rounded-full object-cover" alt="" />
|
||||||
|
<User v-else class="size-5 text-slate-500" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<textarea
|
||||||
|
v-model="commentContent"
|
||||||
|
@input="triggerAutoSaveDraft"
|
||||||
|
placeholder="Viết bình luận..."
|
||||||
|
rows="3"
|
||||||
|
maxlength="1000"
|
||||||
|
class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-3 py-2.5 text-sm text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-slate-500">Công khai</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="isAnonymous = !isAnonymous"
|
||||||
|
class="relative w-9 h-5 rounded-full transition-colors"
|
||||||
|
:class="isAnonymous ? 'bg-cinek-500' : 'bg-white/20'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow transition-transform"
|
||||||
|
:class="isAnonymous ? 'translate-x-4' : 'translate-x-0'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs" :class="isAnonymous ? 'text-cinek-400' : 'text-slate-500'">Ẩn danh</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-xs text-slate-600">{{ commentContent.length }} / 1000</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="!commentContent.trim() || isCommentSubmitting"
|
||||||
|
@click="handleSubmitComment"
|
||||||
|
class="flex items-center gap-1.5 rounded-lg bg-cinek-500 px-4 py-1.5 text-xs font-bold text-slate-950 hover:bg-cinek-400 disabled:cursor-not-allowed disabled:opacity-40 transition"
|
||||||
|
>
|
||||||
|
<Loader2 v-if="isCommentSubmitting" class="size-3.5 animate-spin" />
|
||||||
|
<Send class="size-3.5" v-else />
|
||||||
|
<span>Gửi</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasDraft" class="mt-2 flex items-center gap-2">
|
||||||
|
<span class="text-xs text-yellow-400/70 flex items-center gap-1">
|
||||||
|
<span class="size-1.5 rounded-full bg-yellow-400/50 inline-block" />
|
||||||
|
Có nháp đã lưu
|
||||||
|
</span>
|
||||||
|
<button type="button" @click="handleDeleteDraft" class="text-xs text-slate-500 hover:text-red-400 transition">Xóa nháp</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isCommentsLoading" class="space-y-4">
|
||||||
|
<div v-for="i in 2" :key="i" class="flex items-start gap-3 p-4 bg-[#13151f] rounded-xl animate-pulse">
|
||||||
|
<div class="size-10 rounded-full bg-white/5 shrink-0" />
|
||||||
|
<div class="flex-1 space-y-2">
|
||||||
|
<div class="h-3.5 bg-white/5 rounded w-20" />
|
||||||
|
<div class="h-3 bg-white/5 rounded w-full" />
|
||||||
|
<div class="h-3 bg-white/5 rounded w-3/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="comments.length" class="space-y-1">
|
||||||
|
<div v-for="comment in comments" :key="comment.id">
|
||||||
|
<div v-if="comment.pinned" class="flex items-center gap-1.5 mb-2 px-2">
|
||||||
|
<span class="text-xs font-bold text-cinek-400">📌 Ghim bởi Admin</span>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl p-4 border-t border-white/5" :class="comment.pinned ? 'bg-yellow-500/10 border border-yellow-500/20' : ''">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="shrink-0 relative">
|
||||||
|
<div class="size-10 rounded-full bg-white/10 flex items-center justify-center" :class="comment.userRole === 'admin' && !comment.anonymous ? 'ring-2 ring-yellow-400 ring-offset-2 ring-offset-[#0d0f17]' : ''">
|
||||||
|
<img v-if="comment.userAvatar && !comment.anonymous" :src="comment.userAvatar" class="size-10 rounded-full object-cover" alt="" />
|
||||||
|
<User v-else class="size-5 text-slate-500" />
|
||||||
|
</div>
|
||||||
|
<div v-if="comment.userRole === 'admin' && !comment.anonymous" class="absolute -top-1 -left-1 size-4 bg-cinek-500 rounded-full flex items-center justify-center">
|
||||||
|
<Crown class="size-2.5 text-slate-950" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mb-1">
|
||||||
|
<span v-if="comment.userRole === 'admin' && !comment.anonymous" class="px-2 py-0.5 rounded text-[10px] font-black bg-yellow-400 text-slate-900 shadow-sm">ADMIN</span>
|
||||||
|
<span class="text-sm font-semibold text-white">{{ comment.userName }}</span>
|
||||||
|
<span v-if="comment.anonymous" class="px-1.5 py-0.5 rounded text-[10px] font-medium bg-white/10 text-slate-400">Ẩn danh</span>
|
||||||
|
<span class="text-xs text-slate-600">{{ timeAgo(comment.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-300 whitespace-pre-wrap break-words">{{ comment.content }}</p>
|
||||||
|
<div class="flex items-center gap-3 mt-2.5">
|
||||||
|
<button type="button" @click="handleVote(comment.id, comment.userVote === 1 ? 0 : 1)" class="flex items-center gap-1 text-xs" :class="comment.userVote === 1 ? 'text-cinek-400' : 'text-slate-500 hover:text-slate-300'">
|
||||||
|
<ThumbsUp class="size-3.5" /><span>{{ comment.likeCount || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="handleVote(comment.id, comment.userVote === -1 ? 0 : -1)" class="flex items-center gap-1 text-xs" :class="comment.userVote === -1 ? 'text-red-400' : 'text-slate-500 hover:text-slate-300'">
|
||||||
|
<ThumbsDown class="size-3.5" /><span>{{ comment.dislikeCount || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="startReply(comment.id)" class="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-300">
|
||||||
|
<CornerDownLeft class="size-3.5" /><span>Trả lời</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="user && (user.id === comment.userId || user.role === 'admin')" type="button" @click="handleDeleteComment(comment.id)" class="flex items-center gap-1.5 text-xs text-slate-600 hover:text-red-400">
|
||||||
|
<Trash2 class="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isReplyingTo === comment.id" class="mt-3 flex items-start gap-2">
|
||||||
|
<div class="size-7 rounded-full bg-white/10 flex items-center justify-center shrink-0"><User class="size-3.5 text-slate-500" /></div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<textarea v-model="replyContent[comment.id]" rows="2" maxlength="1000" class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-3 py-2 text-sm text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition" placeholder="Viết trả lời..." />
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-slate-500">Công khai</span>
|
||||||
|
<button type="button" @click="isAnonymous = !isAnonymous" class="relative w-8 h-4 rounded-full transition-colors" :class="isAnonymous ? 'bg-cinek-500' : 'bg-white/20'">
|
||||||
|
<span class="absolute top-0.5 left-0.5 size-3 rounded-full bg-white shadow transition-transform" :class="isAnonymous ? 'translate-x-4' : 'translate-x-0'" />
|
||||||
|
</button>
|
||||||
|
<span class="text-xs" :class="isAnonymous ? 'text-cinek-400' : 'text-slate-500'">Ẩn danh</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" @click="submitReply(comment.id)" class="px-3 py-1 rounded-lg bg-cinek-500 text-xs font-bold text-slate-950 hover:bg-cinek-400 transition">Trả lời</button>
|
||||||
|
<button type="button" @click="cancelReply" class="px-3 py-1 rounded-lg text-xs text-slate-500 hover:text-white transition">Hủy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="comment.replies?.length" class="mt-4">
|
||||||
|
<button @click="toggleExpand(comment.id)" class="flex items-center gap-1.5 text-xs text-cinek-400 hover:text-cinek-300 transition mb-2">
|
||||||
|
<span>{{ expandedComments.has(comment.id) ? '▲' : '▼' }}</span>
|
||||||
|
<span>{{ expandedComments.has(comment.id) ? 'Ẩn phản hồi' : `Hiển thị ${comment.replies.length} phản hồi` }}</span>
|
||||||
|
</button>
|
||||||
|
<div class="space-y-3 pl-3 border-r-2 border-white/10" :class="!expandedComments.has(comment.id) ? 'max-h-0 overflow-hidden' : ''">
|
||||||
|
<div v-for="reply in comment.replies" :key="reply.id" class="flex items-start gap-2">
|
||||||
|
<div class="shrink-0 relative">
|
||||||
|
<div class="size-7 rounded-full bg-white/10 flex items-center justify-center" :class="reply.userRole === 'admin' && !reply.anonymous ? 'ring-2 ring-yellow-400 ring-offset-1 ring-offset-[#0d0f17]' : ''">
|
||||||
|
<img v-if="reply.userAvatar && !reply.anonymous" :src="reply.userAvatar" class="size-7 rounded-full object-cover" alt="" />
|
||||||
|
<User v-else class="size-3.5 text-slate-500" />
|
||||||
|
</div>
|
||||||
|
<div v-if="reply.userRole === 'admin' && !reply.anonymous" class="absolute -top-0.5 -left-0.5 size-3.5 bg-cinek-500 rounded-full flex items-center justify-center">
|
||||||
|
<Crown class="size-2 text-slate-950" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mb-0.5">
|
||||||
|
<span v-if="reply.userRole === 'admin' && !reply.anonymous" class="px-1.5 py-0.5 rounded text-[9px] font-black bg-yellow-400 text-slate-900">ADMIN</span>
|
||||||
|
<span class="text-xs font-semibold text-white">{{ reply.userName }}</span>
|
||||||
|
<span v-if="reply.anonymous" class="px-1 py-0.5 rounded text-[9px] font-medium bg-white/10 text-slate-400">Ẩn danh</span>
|
||||||
|
<span class="text-xs text-slate-600">{{ timeAgo(reply.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-400 whitespace-pre-wrap break-words">{{ reply.content }}</p>
|
||||||
|
<div class="flex items-center gap-3 mt-1.5">
|
||||||
|
<button type="button" @click="handleVote(reply.id, reply.userVote === 1 ? 0 : 1, true, comment.id)" class="flex items-center gap-1 text-xs" :class="reply.userVote === 1 ? 'text-cinek-400' : 'text-slate-600 hover:text-slate-400'">
|
||||||
|
<ThumbsUp class="size-3" /><span>{{ reply.likeCount || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="handleVote(reply.id, reply.userVote === -1 ? 0 : -1, true, comment.id)" class="flex items-center gap-1 text-xs" :class="reply.userVote === -1 ? 'text-red-400' : 'text-slate-600 hover:text-slate-400'">
|
||||||
|
<ThumbsDown class="size-3" /><span>{{ reply.dislikeCount || 0 }}</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="startReply(reply.id)" class="flex items-center gap-1 text-xs text-slate-600 hover:text-slate-400">
|
||||||
|
<CornerDownLeft class="size-3" /><span>Trả lời</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="user && (user.id === reply.userId || user.role === 'admin')" type="button" @click="handleDeleteComment(reply.id)" class="text-xs text-slate-600 hover:text-red-400">
|
||||||
|
<Trash2 class="size-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="isReplyingTo === reply.id" class="mt-2 flex items-start gap-2">
|
||||||
|
<div class="size-6 rounded-full bg-white/10 flex items-center justify-center shrink-0"><User class="size-3 text-slate-500" /></div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<textarea v-model="replyContent[reply.id]" rows="2" maxlength="1000" class="w-full rounded-lg bg-[#0d0f17] border border-white/10 px-2 py-1.5 text-xs text-white placeholder:text-slate-600 focus:border-cinek-500/50 focus:outline-none resize-none transition" placeholder="Viết trả lời..." />
|
||||||
|
<div class="flex items-center gap-2 mt-1.5">
|
||||||
|
<button type="button" @click="submitReply(reply.id)" class="px-2 py-0.5 rounded bg-cinek-500 text-[10px] font-bold text-slate-950 hover:bg-cinek-400 transition">Trả lời</button>
|
||||||
|
<button type="button" @click="cancelReply" class="px-2 py-0.5 rounded text-[10px] text-slate-500 hover:text-white transition">Hủy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<MessageSquare class="size-10 text-white/10 mb-3" />
|
||||||
|
<p class="text-slate-400 text-sm">Chưa có bình luận nào.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
export type CommentDraft = {
|
||||||
|
slug: string
|
||||||
|
source: string
|
||||||
|
content: string
|
||||||
|
movieName?: string
|
||||||
|
savedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftKey = 'cinek-comment-drafts'
|
||||||
|
|
||||||
|
function readDrafts(): CommentDraft[] {
|
||||||
|
if (!import.meta.client) return []
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(draftKey)
|
||||||
|
const drafts = raw ? JSON.parse(raw) : []
|
||||||
|
return Array.isArray(drafts) ? drafts : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDrafts(drafts: CommentDraft[]) {
|
||||||
|
if (!import.meta.client) return
|
||||||
|
window.localStorage.setItem(draftKey, JSON.stringify(drafts))
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDraftKey(slug: string, source: string) {
|
||||||
|
return `${source}::${slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommentDraft() {
|
||||||
|
function getDraft(slug: string, source: string): CommentDraft | null {
|
||||||
|
const drafts = readDrafts()
|
||||||
|
const key = findDraftKey(slug, source)
|
||||||
|
return drafts.find(d => findDraftKey(d.slug, d.source) === key) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDraft(slug: string, source: string, content: string, movieName?: string) {
|
||||||
|
const drafts = readDrafts()
|
||||||
|
const key = findDraftKey(slug, source)
|
||||||
|
const filtered = drafts.filter(d => findDraftKey(d.slug, d.source) !== key)
|
||||||
|
filtered.unshift({ slug, source, content, movieName, savedAt: Date.now() })
|
||||||
|
writeDrafts(filtered.slice(0, 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDraft(slug: string, source: string) {
|
||||||
|
const drafts = readDrafts()
|
||||||
|
const key = findDraftKey(slug, source)
|
||||||
|
const filtered = drafts.filter(d => findDraftKey(d.slug, d.source) !== key)
|
||||||
|
writeDrafts(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllDrafts(): CommentDraft[] {
|
||||||
|
return readDrafts()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllDrafts() {
|
||||||
|
if (import.meta.client) {
|
||||||
|
window.localStorage.removeItem(draftKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearExpiredDrafts(maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
|
||||||
|
const drafts = readDrafts().filter(d => Date.now() - d.savedAt < maxAgeMs)
|
||||||
|
writeDrafts(drafts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getDraft, saveDraft, deleteDraft, getAllDrafts, clearAllDrafts, clearExpiredDrafts }
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
export type CommentReply = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
userName: string
|
||||||
|
userAvatar?: string
|
||||||
|
parentId: number
|
||||||
|
content: string
|
||||||
|
likeCount: number
|
||||||
|
dislikeCount: number
|
||||||
|
createdAt: string
|
||||||
|
userVote: number
|
||||||
|
pinned: boolean
|
||||||
|
spoiler: boolean
|
||||||
|
anonymous: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Comment = {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
userName: string
|
||||||
|
userAvatar?: string
|
||||||
|
source: string
|
||||||
|
slug: string
|
||||||
|
movieName?: string
|
||||||
|
content: string
|
||||||
|
likeCount: number
|
||||||
|
dislikeCount: number
|
||||||
|
createdAt: string
|
||||||
|
userVote: number
|
||||||
|
replies: CommentReply[]
|
||||||
|
pinned: boolean
|
||||||
|
spoiler: boolean
|
||||||
|
anonymous: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useComments() {
|
||||||
|
async function fetchComments(source: string, slug: string, userId?: number) {
|
||||||
|
try {
|
||||||
|
const data = await $fetch('/api/comments', {
|
||||||
|
query: { source, slug, userId },
|
||||||
|
})
|
||||||
|
return data.items as Comment[]
|
||||||
|
} catch {
|
||||||
|
return [] as Comment[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postComment(
|
||||||
|
source: string,
|
||||||
|
slug: string,
|
||||||
|
content: string,
|
||||||
|
movieName?: string,
|
||||||
|
parentId?: number,
|
||||||
|
spoiler?: boolean,
|
||||||
|
anonymous?: boolean,
|
||||||
|
) {
|
||||||
|
return $fetch('/api/comments', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { source, slug, content, movieName, parentId, spoiler, anonymous },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteComment(id: number) {
|
||||||
|
return $fetch(`/api/comments/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function voteComment(commentId: number, vote: number) {
|
||||||
|
return $fetch('/api/comments/vote', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { commentId, vote },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function togglePinComment(id: number) {
|
||||||
|
return $fetch(`/api/comments/${id}/pin`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { fetchComments, postComment, deleteComment, voteComment, togglePinComment }
|
||||||
|
}
|
||||||
+15
-37
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronDown, Heart, MessageCircle, Play, Plus, Server, Share2, Star, Video } from 'lucide-vue-next'
|
import { ChevronDown, ChevronRight, Heart, MessageSquare, Play, Plus, Server, Share2, Star, Video } from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const requestedSource = computed(() => String(route.query.source || 'nguonc'))
|
const requestedSource = computed(() => String(route.query.source || 'nguonc'))
|
||||||
@@ -31,26 +31,8 @@ const activeServer = computed(() => servers.value[selectedServer.value])
|
|||||||
const activeSource = computed(() => String(activeServer.value?.source || movie.value?.source || route.query.source || 'nguonc'))
|
const activeSource = computed(() => String(activeServer.value?.source || movie.value?.source || route.query.source || 'nguonc'))
|
||||||
const activeSourceSlug = computed(() => String(activeServer.value?.sourceSlug || movie.value?.slug || route.params.slug))
|
const activeSourceSlug = computed(() => String(activeServer.value?.sourceSlug || movie.value?.slug || route.params.slug))
|
||||||
const activeSourceServerIndex = computed(() => Number(activeServer.value?.sourceServerIndex ?? 0))
|
const activeSourceServerIndex = computed(() => Number(activeServer.value?.sourceServerIndex ?? 0))
|
||||||
const sourceOptions = computed(() =>
|
|
||||||
(movie.value?.sources || [{ source: activeSource.value, slug: activeSourceSlug.value }])
|
|
||||||
.map((source: any) => ({
|
|
||||||
label: String(source.source || '').toUpperCase(),
|
|
||||||
value: source.source,
|
|
||||||
slug: source.slug,
|
|
||||||
}))
|
|
||||||
.filter((source: any) => source.value && source.slug),
|
|
||||||
)
|
|
||||||
const actors = computed(() => movie.value?.actors ?? [])
|
const actors = computed(() => movie.value?.actors ?? [])
|
||||||
const actorSummary = computed(() => actors.value.map((actor: any) => actor.name).filter(Boolean).slice(0, 6).join(', '))
|
const actorSummary = computed(() => actors.value.map((actor: any) => actor.name).filter(Boolean).slice(0, 6).join(', '))
|
||||||
const firstWatchLink = computed(() => ({
|
|
||||||
path: `/xem/${activeSourceSlug.value}`,
|
|
||||||
query: {
|
|
||||||
source: activeSource.value,
|
|
||||||
srcs: requestedSources.value || undefined,
|
|
||||||
server: activeSourceServerIndex.value,
|
|
||||||
ep: 1,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
const episodeCount = computed(() => {
|
const episodeCount = computed(() => {
|
||||||
const total = servers.value.reduce((t: number, s: any) => t + (s.episodes?.length || 0), 0)
|
const total = servers.value.reduce((t: number, s: any) => t + (s.episodes?.length || 0), 0)
|
||||||
return total || undefined
|
return total || undefined
|
||||||
@@ -67,6 +49,14 @@ const libraryItem = computed(() => {
|
|||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const firstWatchLink = computed(() => ({
|
||||||
|
path: `/xem/${activeSourceSlug.value}`,
|
||||||
|
query: { source: activeSource.value, srcs: requestedSources.value || undefined, server: activeSourceServerIndex.value, ep: 1 },
|
||||||
|
}))
|
||||||
|
|
||||||
|
function actorInitial(name: string) {
|
||||||
|
return name.trim().charAt(0).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
function episodeLink(index: number) {
|
function episodeLink(index: number) {
|
||||||
return {
|
return {
|
||||||
@@ -102,10 +92,6 @@ function formatEpisodeName(name: string, index: number) {
|
|||||||
return /^\d+$/.test(label) ? `Tập ${label}` : label
|
return /^\d+$/.test(label) ? `Tập ${label}` : label
|
||||||
}
|
}
|
||||||
|
|
||||||
function actorInitial(name: string) {
|
|
||||||
return name.trim().charAt(0).toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function flashActionMessage(message: string) {
|
function flashActionMessage(message: string) {
|
||||||
actionMessage.value = message
|
actionMessage.value = message
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -159,6 +145,8 @@ async function shareMovie() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await refreshFavoriteState()
|
await refreshFavoriteState()
|
||||||
|
// loadCommentDraft()
|
||||||
|
// await loadComments()
|
||||||
await $fetch(`/api/movies/${route.params.slug}/view`, {
|
await $fetch(`/api/movies/${route.params.slug}/view`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
query: { source: route.query.source },
|
query: { source: route.query.source },
|
||||||
@@ -426,7 +414,7 @@ useHead(() => ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs: Episodes / Actors (inside info column) -->
|
<!-- Tabs: Episodes / Actors (inside info column) -->
|
||||||
<div class="mt-6 border-t border-white/10 pt-6">
|
<div class="mt-6 border-white/10 pt-6">
|
||||||
<div class="flex items-center gap-6 border-b border-white/10 mb-6 overflow-x-auto">
|
<div class="flex items-center gap-6 border-b border-white/10 mb-6 overflow-x-auto">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="pb-3 text-[13px] md:text-[15px] font-semibold transition-colors relative whitespace-nowrap"
|
class="pb-3 text-[13px] md:text-[15px] font-semibold transition-colors relative whitespace-nowrap"
|
||||||
@@ -511,19 +499,9 @@ useHead(() => ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ClientOnly>
|
||||||
<!-- Comment Section -->
|
<CommentSection :source="activeSource" :slug="String(route.params.slug)" :movie-name="movie?.name" />
|
||||||
<div class="mt-10 border-t border-white/10 pt-8">
|
</ClientOnly>
|
||||||
<div class="flex items-center gap-3 mb-6">
|
|
||||||
<MessageCircle class="size-5 text-yellow-300" />
|
|
||||||
<h2 class="text-xl font-bold text-white">Bình luận</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col items-center justify-center py-10 text-center border border-white/10 rounded-xl bg-[#191b24]">
|
|
||||||
<MessageCircle class="size-10 text-slate-500 mb-3" />
|
|
||||||
<p class="text-sm text-slate-400">Tính năng bình luận đang được phát triển.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+15
-36
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type Hls from 'hls.js'
|
import type Hls from 'hls.js'
|
||||||
import {
|
import {
|
||||||
Bug, CircleHelp, FastForward, Heart, Layers, Loader2, Maximize, Pause,
|
AlertTriangle, BadgeCheck, Bug, ChevronRight, CircleHelp, CornerDownLeft, Crown, Eye, EyeOff, FastForward, Heart, Image, Layers,
|
||||||
PictureInPicture2, Play, Plus, Rewind, Settings, Share2, SkipForward,
|
Loader2, LogIn, Maximize, MessageCircle, MessageSquare, Pause, PictureInPicture2, Pin, Play, Plus, Rewind,
|
||||||
Star, Volume2, VolumeX, Captions, Languages, ChevronDown, ChevronRight,
|
Send, Settings, Share2, SkipForward, Star, ThumbsDown, ThumbsUp, Trash2, User, Volume2, VolumeX, Captions, Languages,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -55,29 +55,17 @@ const { data: movie, pending, error } = useFetch(() => `/api/movies/${route.para
|
|||||||
watch: [() => route.query.source, () => route.query.srcs],
|
watch: [() => route.query.source, () => route.query.srcs],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentMovieSource = computed(() => String(route.query.source || movie.value?.source || ''))
|
||||||
|
const libraryItem = computed(() => movie.value ? {
|
||||||
|
source: currentMovieSource.value, slug: String(route.params.slug),
|
||||||
|
name: movie.value.name, originName: movie.value.originName, thumb: movie.value.thumb, poster: movie.value.poster, updatedAt: Date.now(),
|
||||||
|
} : null)
|
||||||
|
|
||||||
const servers = computed(() => movie.value?.servers ?? [])
|
const servers = computed(() => movie.value?.servers ?? [])
|
||||||
const activeServer = computed(() => servers.value[selectedServer.value] ?? servers.value[0])
|
const activeServer = computed(() => servers.value[selectedServer.value] ?? servers.value[0])
|
||||||
const activeSubtitle = computed(() => activeServer.value?.channels?.[selectedSubtitle.value] ?? activeServer.value)
|
const activeSubtitle = computed(() => activeServer.value?.channels?.[selectedSubtitle.value] ?? activeServer.value)
|
||||||
const activeEpisode = computed(() => activeSubtitle.value?.episodes?.[selectedEpisode.value] ?? activeSubtitle.value?.episodes?.[0])
|
const activeEpisode = computed(() => activeSubtitle.value?.episodes?.[selectedEpisode.value] ?? activeSubtitle.value?.episodes?.[0])
|
||||||
const hlsPlayerUrl = computed(() => activeEpisode.value?.linkM3u8 || '')
|
|
||||||
const actorSummary = computed(() => (movie.value?.actors ?? []).map((a: any) => a.name).filter(Boolean).slice(0, 6).join(', '))
|
|
||||||
const durationSeconds = computed(() => Math.floor(videoDuration.value || parseDurationSeconds(movie.value?.time || '')))
|
|
||||||
const hasNextEpisode = computed(() => Boolean(activeSubtitle.value?.episodes?.[selectedEpisode.value + 1]))
|
|
||||||
const episodeProgress = computed(() => {
|
|
||||||
const total = movie.value?.episodeTotal ? Number(String(movie.value.episodeTotal).replace(/[^0-9]/g, '')) : 0
|
|
||||||
const available = activeSubtitle.value?.episodes?.length || 0
|
|
||||||
return { available, total }
|
|
||||||
})
|
|
||||||
|
|
||||||
function getProxyUrl(url: string) { const encoded = btoa(url); return `/api/proxy-m3u8/${encoded}` }
|
|
||||||
const progressPercent = computed(() => durationSeconds.value && progressSeconds.value ? Math.min(Math.max((progressSeconds.value / durationSeconds.value) * 100, 0), 100) : 0)
|
|
||||||
const shouldShowHlsControls = computed(() => (controlsVisible.value || !isVideoPlaying.value) && !(isHlsFullscreen.value && edgeProgressVisible.value && isVideoPlaying.value))
|
|
||||||
const skipIntroSeconds = 85
|
|
||||||
const skipOutroSeconds = computed(() => Math.max((durationSeconds.value || 0) - 85, 0))
|
|
||||||
const libraryItem = computed(() => movie.value ? {
|
|
||||||
source: String(route.query.source || movie.value.source || ''), slug: String(route.params.slug),
|
|
||||||
name: movie.value.name, originName: movie.value.originName, thumb: movie.value.thumb, poster: movie.value.poster, updatedAt: Date.now(),
|
|
||||||
} : null)
|
|
||||||
|
|
||||||
// Related movies
|
// Related movies
|
||||||
const { data: relatedData } = await useFetch('/api/movies', {
|
const { data: relatedData } = await useFetch('/api/movies', {
|
||||||
@@ -414,6 +402,7 @@ onBeforeUnmount(() => {
|
|||||||
if (hasStarted.value) saveWatchHistory(); stopProgressTimer()
|
if (hasStarted.value) saveWatchHistory(); stopProgressTimer()
|
||||||
if (controlsHideTimer) clearTimeout(controlsHideTimer); if (edgeProgressTimer) clearTimeout(edgeProgressTimer)
|
if (controlsHideTimer) clearTimeout(controlsHideTimer); if (edgeProgressTimer) clearTimeout(edgeProgressTimer)
|
||||||
if (singleTapTimer) clearTimeout(singleTapTimer); if (holdSpeedTimer) clearTimeout(holdSpeedTimer)
|
if (singleTapTimer) clearTimeout(singleTapTimer); if (holdSpeedTimer) clearTimeout(holdSpeedTimer)
|
||||||
|
if (draftAutoSaveTimer) clearTimeout(draftAutoSaveTimer)
|
||||||
clearHlsFallbackTimer(); destroyHlsPlayer()
|
clearHlsFallbackTimer(); destroyHlsPlayer()
|
||||||
if (import.meta.client) { document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('keydown', handleKeyboardShortcut); document.removeEventListener('fullscreenchange', updateFullscreenState); document.removeEventListener('webkitfullscreenchange', updateFullscreenState); window.removeEventListener('orientationchange', handleOrientationFullscreen); window.removeEventListener('resize', handleOrientationFullscreen); screen.orientation?.removeEventListener?.('change', handleOrientationFullscreen) }
|
if (import.meta.client) { document.removeEventListener('visibilitychange', handleVisibilityChange); document.removeEventListener('keydown', handleKeyboardShortcut); document.removeEventListener('fullscreenchange', updateFullscreenState); document.removeEventListener('webkitfullscreenchange', updateFullscreenState); window.removeEventListener('orientationchange', handleOrientationFullscreen); window.removeEventListener('resize', handleOrientationFullscreen); screen.orientation?.removeEventListener?.('change', handleOrientationFullscreen) }
|
||||||
})
|
})
|
||||||
@@ -665,7 +654,7 @@ useHead(() => ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Episode Progress -->
|
<!-- Episode Progress -->
|
||||||
<div v-if="episodeProgress.total"
|
<div v-if="episodeProgress?.total"
|
||||||
class="inline-flex self-center sm:self-start items-center gap-1.5 px-3 py-1.5 rounded-full mt-2 border bg-cinek-500/10 border-cinek-500/20 text-cinek-500">
|
class="inline-flex self-center sm:self-start items-center gap-1.5 px-3 py-1.5 rounded-full mt-2 border bg-cinek-500/10 border-cinek-500/20 text-cinek-500">
|
||||||
<Loader2 class="size-3.5 animate-spin" />
|
<Loader2 class="size-3.5 animate-spin" />
|
||||||
<span class="text-xs font-medium">Đã chiếu: {{ episodeProgress.available }} / {{ episodeProgress.total
|
<span class="text-xs font-medium">Đã chiếu: {{ episodeProgress.available }} / {{ episodeProgress.total
|
||||||
@@ -769,20 +758,9 @@ useHead(() => ({
|
|||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Comment Section -->
|
<ClientOnly>
|
||||||
<div class="mb-12">
|
<CommentSection :source="currentMovieSource" :slug="String(route.params.slug)" :movie-name="movie?.name" />
|
||||||
<div class="flex items-center gap-4 mb-6">
|
</ClientOnly>
|
||||||
<div class="flex items-center gap-2.5">
|
|
||||||
<h2 class="text-xl font-bold text-white">Bình luận</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-3 mb-8 p-4 bg-[#1a1a24] border border-white/10 rounded-xl">
|
|
||||||
<p class="text-sm text-slate-400">Đăng nhập để bình luận về phim này.</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-center">
|
|
||||||
<p class="text-slate-400 text-sm">Chưa có bình luận nào.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
</main>
|
</main>
|
||||||
@@ -916,4 +894,5 @@ useHead(() => ({
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(90deg, rgb(250 204 21) v-bind('`${progressPercent}%`'), transparent 0);
|
background: linear-gradient(90deg, rgb(250 204 21) v-bind('`${progressPercent}%`'), transparent 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -102,3 +102,33 @@ with check (auth.uid() = user_id);
|
|||||||
create policy "Users can delete own watch later movies"
|
create policy "Users can delete own watch later movies"
|
||||||
on public.watch_later_movies for delete
|
on public.watch_later_movies for delete
|
||||||
using (auth.uid() = user_id);
|
using (auth.uid() = user_id);
|
||||||
|
|
||||||
|
create table public.comments (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
user_id uuid not null references auth.users(id) on delete cascade,
|
||||||
|
source text not null default '',
|
||||||
|
slug text not null,
|
||||||
|
movie_name text,
|
||||||
|
content text not null,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table public.comments enable row level security;
|
||||||
|
|
||||||
|
create policy "Anyone can read comments"
|
||||||
|
on public.comments for select
|
||||||
|
using (true);
|
||||||
|
|
||||||
|
create policy "Authenticated users can insert comments"
|
||||||
|
on public.comments for insert
|
||||||
|
with check (auth.uid() = user_id);
|
||||||
|
|
||||||
|
create policy "Users can update own comments"
|
||||||
|
on public.comments for update
|
||||||
|
using (auth.uid() = user_id)
|
||||||
|
with check (auth.uid() = user_id);
|
||||||
|
|
||||||
|
create policy "Users can delete own comments"
|
||||||
|
on public.comments for delete
|
||||||
|
using (auth.uid() = user_id);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { comments } from '../../database/schema'
|
||||||
|
import { getTokenFromEvent, verifyToken } from '../../utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getTokenFromEvent(event)
|
||||||
|
if (!token) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Chưa đăng nhập' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
if (!payload) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Phiên đăng nhập hết hạn' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(getRouterParam(event, 'id'))
|
||||||
|
if (!id) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Thiếu ID bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
|
||||||
|
const [comment] = await db
|
||||||
|
.select()
|
||||||
|
.from(comments)
|
||||||
|
.where(eq(comments.id, id))
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Không tìm thấy bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.userId !== payload.id && payload.role !== 'admin') {
|
||||||
|
throw createError({ statusCode: 403, message: 'Không có quyền xóa bình luận này' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(comments)
|
||||||
|
.where(eq(comments.id, id))
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { comments } from '../../../database/schema'
|
||||||
|
import { getTokenFromEvent, verifyToken } from '../../../utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getTokenFromEvent(event)
|
||||||
|
if (!token) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Chưa đăng nhập' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
if (!payload) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Phiên đăng nhập hết hạn' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admins can pin/unpin
|
||||||
|
if (payload.role !== 'admin') {
|
||||||
|
throw createError({ statusCode: 403, message: 'Chỉ admin mới có quyền ghim bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(getRouterParam(event, 'id'))
|
||||||
|
if (!id) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Thiếu ID bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
|
||||||
|
const [comment] = await db
|
||||||
|
.select()
|
||||||
|
.from(comments)
|
||||||
|
.where(eq(comments.id, id))
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Không tìm thấy bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle pinned state
|
||||||
|
const newPinned = !comment.pinned
|
||||||
|
await db
|
||||||
|
.update(comments)
|
||||||
|
.set({ pinned: newPinned })
|
||||||
|
.where(eq(comments.id, id))
|
||||||
|
|
||||||
|
return { success: true, pinned: newPinned }
|
||||||
|
})
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { eq, and, desc, sql, or, isNull } from 'drizzle-orm'
|
||||||
|
import { comments, users, commentVotes } from '../../database/schema'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const query = getQuery(event)
|
||||||
|
const { source = '', slug, userId } = query
|
||||||
|
|
||||||
|
if (!slug) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Thiếu slug phim' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
|
||||||
|
const whereConditions = [
|
||||||
|
eq(comments.slug, String(slug)),
|
||||||
|
isNull(comments.parentId),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
whereConditions.push(eq(comments.source, String(source)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch pinned comments first, then regular ones
|
||||||
|
const results = await db
|
||||||
|
.select({
|
||||||
|
id: comments.id,
|
||||||
|
userId: comments.userId,
|
||||||
|
userName: users.name,
|
||||||
|
userAvatar: users.avatar,
|
||||||
|
userRole: users.role,
|
||||||
|
source: comments.source,
|
||||||
|
slug: comments.slug,
|
||||||
|
movieName: comments.movieName,
|
||||||
|
content: comments.content,
|
||||||
|
pinned: comments.pinned,
|
||||||
|
spoiler: comments.spoiler,
|
||||||
|
anonymous: comments.anonymous,
|
||||||
|
likeCount: comments.likeCount,
|
||||||
|
dislikeCount: comments.dislikeCount,
|
||||||
|
createdAt: comments.createdAt,
|
||||||
|
})
|
||||||
|
.from(comments)
|
||||||
|
.leftJoin(users, eq(comments.userId, users.id))
|
||||||
|
.where(and(...whereConditions))
|
||||||
|
.orderBy(sql`${comments.pinned} DESC, ${comments.createdAt} DESC`)
|
||||||
|
.limit(100)
|
||||||
|
|
||||||
|
const commentIds = results.map(r => r.id)
|
||||||
|
|
||||||
|
let userVotes: Record<number, number> = {}
|
||||||
|
if (userId && commentIds.length > 0) {
|
||||||
|
const votes = await db
|
||||||
|
.select()
|
||||||
|
.from(commentVotes)
|
||||||
|
.where(and(
|
||||||
|
eq(commentVotes.userId, Number(userId)),
|
||||||
|
or(...commentIds.map(id => eq(commentVotes.commentId, id)))
|
||||||
|
))
|
||||||
|
userVotes = Object.fromEntries(votes.map(v => [v.commentId, v.vote]))
|
||||||
|
}
|
||||||
|
|
||||||
|
let replies: any[] = []
|
||||||
|
if (commentIds.length > 0) {
|
||||||
|
replies = await db
|
||||||
|
.select({
|
||||||
|
id: comments.id,
|
||||||
|
userId: comments.userId,
|
||||||
|
userName: users.name,
|
||||||
|
userAvatar: users.avatar,
|
||||||
|
userRole: users.role,
|
||||||
|
parentId: comments.parentId,
|
||||||
|
content: comments.content,
|
||||||
|
pinned: comments.pinned,
|
||||||
|
spoiler: comments.spoiler,
|
||||||
|
anonymous: comments.anonymous,
|
||||||
|
likeCount: comments.likeCount,
|
||||||
|
dislikeCount: comments.dislikeCount,
|
||||||
|
createdAt: comments.createdAt,
|
||||||
|
})
|
||||||
|
.from(comments)
|
||||||
|
.leftJoin(users, eq(comments.userId, users.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(...commentIds.map(id => eq(comments.parentId, id)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(comments.createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const repliesByParent: Record<number, typeof replies> = {}
|
||||||
|
for (const reply of replies) {
|
||||||
|
const parentId = reply.parentId!
|
||||||
|
if (!repliesByParent[parentId]) {
|
||||||
|
repliesByParent[parentId] = []
|
||||||
|
}
|
||||||
|
repliesByParent[parentId].push(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: results.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
userId: r.userId,
|
||||||
|
userName: r.anonymous ? 'Ẩn danh' : (r.userName || 'Ẩn danh'),
|
||||||
|
userAvatar: r.anonymous ? null : r.userAvatar,
|
||||||
|
userRole: r.userRole,
|
||||||
|
source: r.source,
|
||||||
|
slug: r.slug,
|
||||||
|
movieName: r.movieName,
|
||||||
|
content: r.content,
|
||||||
|
pinned: r.pinned || false,
|
||||||
|
spoiler: r.spoiler || false,
|
||||||
|
anonymous: r.anonymous || false,
|
||||||
|
likeCount: r.likeCount,
|
||||||
|
dislikeCount: r.dislikeCount,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
userVote: userVotes[r.id] || 0,
|
||||||
|
replies: (repliesByParent[r.id] || []).map(rep => ({
|
||||||
|
id: rep.id,
|
||||||
|
userId: rep.userId,
|
||||||
|
userName: rep.anonymous ? 'Ẩn danh' : (rep.userName || 'Ẩn danh'),
|
||||||
|
userAvatar: rep.anonymous ? null : rep.userAvatar,
|
||||||
|
userRole: rep.userRole,
|
||||||
|
parentId: rep.parentId,
|
||||||
|
content: rep.content,
|
||||||
|
pinned: rep.pinned || false,
|
||||||
|
spoiler: rep.spoiler || false,
|
||||||
|
anonymous: rep.anonymous || false,
|
||||||
|
likeCount: rep.likeCount,
|
||||||
|
dislikeCount: rep.dislikeCount,
|
||||||
|
createdAt: rep.createdAt,
|
||||||
|
userVote: userVotes[rep.id] || 0,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { comments } from '../../database/schema'
|
||||||
|
import { getTokenFromEvent, verifyToken } from '../../utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getTokenFromEvent(event)
|
||||||
|
if (!token) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Chưa đăng nhập' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
if (!payload) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Phiên đăng nhập hết hạn' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { source, slug, content, movieName, parentId, spoiler, anonymous } = body
|
||||||
|
|
||||||
|
if (!slug || !content?.trim()) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Thiếu thông tin bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentId) {
|
||||||
|
const db = useDb()
|
||||||
|
const [parent] = await db.select().from(comments).where(eq(comments.id, Number(parentId)))
|
||||||
|
if (!parent) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Không tìm thấy bình luận gốc' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
|
||||||
|
const result = await db.insert(comments).values({
|
||||||
|
userId: payload.id,
|
||||||
|
source: source || '',
|
||||||
|
slug,
|
||||||
|
movieName: movieName || null,
|
||||||
|
content: content.trim(),
|
||||||
|
parentId: parentId ? Number(parentId) : null,
|
||||||
|
spoiler: spoiler ? true : false,
|
||||||
|
anonymous: anonymous ? true : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: result.insertId,
|
||||||
|
userId: payload.id,
|
||||||
|
source: source || '',
|
||||||
|
slug,
|
||||||
|
movieName: movieName || null,
|
||||||
|
content: content.trim(),
|
||||||
|
spoiler: spoiler ? true : false,
|
||||||
|
anonymous: anonymous ? true : false,
|
||||||
|
parentId: parentId ? Number(parentId) : null,
|
||||||
|
likeCount: 0,
|
||||||
|
dislikeCount: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { eq, and, sql } from 'drizzle-orm'
|
||||||
|
import { comments, commentVotes } from '../../database/schema'
|
||||||
|
import { getTokenFromEvent, verifyToken } from '../../utils/auth'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const token = getTokenFromEvent(event)
|
||||||
|
if (!token) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Chưa đăng nhập' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
if (!payload) {
|
||||||
|
throw createError({ statusCode: 401, message: 'Phiên đăng nhập hết hạn' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { commentId, vote } = body
|
||||||
|
|
||||||
|
if (!commentId || ![-1, 0, 1].includes(vote)) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Thiếu thông tin vote' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = useDb()
|
||||||
|
|
||||||
|
const [comment] = await db.select().from(comments).where(eq(comments.id, Number(commentId)))
|
||||||
|
if (!comment) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Không tìm thấy bình luận' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existingVote] = await db
|
||||||
|
.select()
|
||||||
|
.from(commentVotes)
|
||||||
|
.where(and(
|
||||||
|
eq(commentVotes.userId, payload.id),
|
||||||
|
eq(commentVotes.commentId, Number(commentId))
|
||||||
|
))
|
||||||
|
|
||||||
|
if (existingVote) {
|
||||||
|
if (existingVote.vote === vote) {
|
||||||
|
await db.delete(commentVotes).where(eq(commentVotes.id, existingVote.id))
|
||||||
|
if (vote === 1) {
|
||||||
|
await db.update(comments).set({ likeCount: sql`${comments.likeCount} - 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
} else if (vote === -1) {
|
||||||
|
await db.update(comments).set({ dislikeCount: sql`${comments.dislikeCount} - 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
}
|
||||||
|
return { vote: 0, likeCount: comment.likeCount + (vote === 1 ? -1 : 0), dislikeCount: comment.dislikeCount + (vote === -1 ? -1 : 0) }
|
||||||
|
} else {
|
||||||
|
if (existingVote.vote === 1) {
|
||||||
|
await db.update(comments).set({ likeCount: sql`${comments.likeCount} - 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
} else if (existingVote.vote === -1) {
|
||||||
|
await db.update(comments).set({ dislikeCount: sql`${comments.dislikeCount} - 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.update(commentVotes).set({ vote }).where(eq(commentVotes.id, existingVote.id))
|
||||||
|
|
||||||
|
if (vote === 1) {
|
||||||
|
await db.update(comments).set({ likeCount: sql`${comments.likeCount} + 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
} else if (vote === -1) {
|
||||||
|
await db.update(comments).set({ dislikeCount: sql`${comments.dislikeCount} + 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { vote, likeCount: comment.likeCount + (vote === 1 ? 1 : -1), dislikeCount: comment.dislikeCount + (vote === -1 ? 1 : -1) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (vote !== 0) {
|
||||||
|
await db.insert(commentVotes).values({
|
||||||
|
userId: payload.id,
|
||||||
|
commentId: Number(commentId),
|
||||||
|
vote,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (vote === 1) {
|
||||||
|
await db.update(comments).set({ likeCount: sql`${comments.likeCount} + 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
} else if (vote === -1) {
|
||||||
|
await db.update(comments).set({ dislikeCount: sql`${comments.dislikeCount} + 1` }).where(eq(comments.id, Number(commentId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { vote, likeCount: comment.likeCount + (vote === 1 ? 1 : 0), dislikeCount: comment.dislikeCount + (vote === -1 ? 1 : 0) }
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE `comments` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`source` varchar(50) NOT NULL DEFAULT '',
|
||||||
|
`slug` varchar(500) NOT NULL,
|
||||||
|
`movie_name` varchar(500),
|
||||||
|
`content` text NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||||
|
`updated_at` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `comments_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_source_slug` ON `comments` (`source`,`slug`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_id` ON `comments` (`user_id`);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE `comment_votes` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`comment_id` int NOT NULL,
|
||||||
|
`vote` int NOT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT (now()),
|
||||||
|
CONSTRAINT `comment_votes_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE `comments` ADD `parent_id` int;--> statement-breakpoint
|
||||||
|
ALTER TABLE `comments` ADD `like_count` int DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `comments` ADD `dislike_count` int DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_user_comment` ON `comment_votes` (`user_id`,`comment_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_parent_id` ON `comments` (`parent_id`);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add pinned and spoiler columns to comments table
|
||||||
|
ALTER TABLE `comments` ADD COLUMN `pinned` boolean NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE `comments` ADD COLUMN `spoiler` boolean NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `comments` ADD `anonymous` boolean DEFAULT false NOT NULL;
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "1cd1504d-5067-4116-9c5b-099d403e6326",
|
||||||
|
"prevId": "c5e6711f-1d8a-4d55-8e5f-846bc6d44924",
|
||||||
|
"tables": {
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"movie_name": {
|
||||||
|
"name": "movie_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_source_slug": {
|
||||||
|
"name": "idx_source_slug",
|
||||||
|
"columns": [
|
||||||
|
"source",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comments_id": {
|
||||||
|
"name": "comments_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"movies": {
|
||||||
|
"name": "movies",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"origin_name": {
|
||||||
|
"name": "origin_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"thumb": {
|
||||||
|
"name": "thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"year": {
|
||||||
|
"name": "year",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "time",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode": {
|
||||||
|
"name": "episode",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode_total": {
|
||||||
|
"name": "episode_total",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"name": "quality",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"name": "lang",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"name": "views",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"countries": {
|
||||||
|
"name": "countries",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"name": "sources",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_poster": {
|
||||||
|
"name": "custom_poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_thumb": {
|
||||||
|
"name": "custom_thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_content": {
|
||||||
|
"name": "custom_content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_episodes": {
|
||||||
|
"name": "custom_episodes",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"api_updated_at": {
|
||||||
|
"name": "api_updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"synced_at": {
|
||||||
|
"name": "synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"movies_id": {
|
||||||
|
"name": "movies_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token": {
|
||||||
|
"name": "reset_token",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token_expires": {
|
||||||
|
"name": "reset_token_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "79064b1a-675e-484f-8a47-d4c6ac4445b6",
|
||||||
|
"prevId": "1cd1504d-5067-4116-9c5b-099d403e6326",
|
||||||
|
"tables": {
|
||||||
|
"comment_votes": {
|
||||||
|
"name": "comment_votes",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comment_id": {
|
||||||
|
"name": "comment_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vote": {
|
||||||
|
"name": "vote",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_comment": {
|
||||||
|
"name": "idx_user_comment",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"comment_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comment_votes_id": {
|
||||||
|
"name": "comment_votes_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"movie_name": {
|
||||||
|
"name": "movie_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"like_count": {
|
||||||
|
"name": "like_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"dislike_count": {
|
||||||
|
"name": "dislike_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_source_slug": {
|
||||||
|
"name": "idx_source_slug",
|
||||||
|
"columns": [
|
||||||
|
"source",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_parent_id": {
|
||||||
|
"name": "idx_parent_id",
|
||||||
|
"columns": [
|
||||||
|
"parent_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comments_id": {
|
||||||
|
"name": "comments_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"movies": {
|
||||||
|
"name": "movies",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"origin_name": {
|
||||||
|
"name": "origin_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"thumb": {
|
||||||
|
"name": "thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"year": {
|
||||||
|
"name": "year",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "time",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode": {
|
||||||
|
"name": "episode",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode_total": {
|
||||||
|
"name": "episode_total",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"name": "quality",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"name": "lang",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"name": "views",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"countries": {
|
||||||
|
"name": "countries",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"name": "sources",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_poster": {
|
||||||
|
"name": "custom_poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_thumb": {
|
||||||
|
"name": "custom_thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_content": {
|
||||||
|
"name": "custom_content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_episodes": {
|
||||||
|
"name": "custom_episodes",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"api_updated_at": {
|
||||||
|
"name": "api_updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"synced_at": {
|
||||||
|
"name": "synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"movies_id": {
|
||||||
|
"name": "movies_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token": {
|
||||||
|
"name": "reset_token",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token_expires": {
|
||||||
|
"name": "reset_token_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "8a064b1a-675e-484f-8a47-d4c6ac4445b7",
|
||||||
|
"prevId": "79064b1a-675e-484f-8a47-d4c6ac4445b6",
|
||||||
|
"tables": {
|
||||||
|
"comment_votes": {
|
||||||
|
"name": "comment_votes",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comment_id": {
|
||||||
|
"name": "comment_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vote": {
|
||||||
|
"name": "vote",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_comment": {
|
||||||
|
"name": "idx_user_comment",
|
||||||
|
"columns": ["user_id", "comment_id"],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comment_votes_id": {
|
||||||
|
"name": "comment_votes_id",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"movie_name": {
|
||||||
|
"name": "movie_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"like_count": {
|
||||||
|
"name": "like_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"dislike_count": {
|
||||||
|
"name": "dislike_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"pinned": {
|
||||||
|
"name": "pinned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spoiler": {
|
||||||
|
"name": "spoiler",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_source_slug": {
|
||||||
|
"name": "idx_source_slug",
|
||||||
|
"columns": ["source", "slug"],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": ["user_id"],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_parent_id": {
|
||||||
|
"name": "idx_parent_id",
|
||||||
|
"columns": ["parent_id"],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comments_id": {
|
||||||
|
"name": "comments_id",
|
||||||
|
"columns": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"movies": {
|
||||||
|
"name": "movies",
|
||||||
|
"columns": {
|
||||||
|
"id": { "name": "id", "type": "int", "primaryKey": false, "notNull": true, "autoincrement": true },
|
||||||
|
"source": { "name": "source", "type": "varchar(50)", "primaryKey": false, "notNull": true, "autoincrement": false },
|
||||||
|
"slug": { "name": "slug", "type": "varchar(500)", "primaryKey": false, "notNull": true, "autoincrement": false },
|
||||||
|
"name": { "name": "name", "type": "varchar(500)", "primaryKey": false, "notNull": true, "autoincrement": false },
|
||||||
|
"origin_name": { "name": "origin_name", "type": "varchar(500)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"thumb": { "name": "thumb", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"poster": { "name": "poster", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"year": { "name": "year", "type": "int", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"time": { "name": "time", "type": "varchar(100)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"episode": { "name": "episode", "type": "varchar(100)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"episode_total": { "name": "episode_total", "type": "varchar(100)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"quality": { "name": "quality", "type": "varchar(50)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"lang": { "name": "lang", "type": "varchar(50)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"type": { "name": "type", "type": "varchar(50)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"rating": { "name": "rating", "type": "int", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"views": { "name": "views", "type": "int", "primaryKey": false, "notNull": true, "autoincrement": false, "default": 0 },
|
||||||
|
"content": { "name": "content", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"categories": { "name": "categories", "type": "json", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"countries": { "name": "countries", "type": "json", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"sources": { "name": "sources", "type": "json", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"custom_poster": { "name": "custom_poster", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"custom_thumb": { "name": "custom_thumb", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"custom_content": { "name": "custom_content", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"custom_episodes": { "name": "custom_episodes", "type": "json", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"active": { "name": "active", "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, "default": false },
|
||||||
|
"api_updated_at": { "name": "api_updated_at", "type": "timestamp", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"synced_at": { "name": "synced_at", "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, "default": "(now())" },
|
||||||
|
"created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, "default": "(now())" },
|
||||||
|
"updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, "onUpdate": true, "default": "(now())" }
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"movies_id": { "name": "movies_id", "columns": ["id"] }
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": { "name": "id", "type": "int", "primaryKey": false, "notNull": true, "autoincrement": true },
|
||||||
|
"name": { "name": "name", "type": "varchar(200)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"email": { "name": "email", "type": "varchar(255)", "primaryKey": false, "notNull": true, "autoincrement": false },
|
||||||
|
"password": { "name": "password", "type": "varchar(255)", "primaryKey": false, "notNull": true, "autoincrement": false },
|
||||||
|
"role": { "name": "role", "type": "varchar(50)", "primaryKey": false, "notNull": true, "autoincrement": false, "default": "'user'" },
|
||||||
|
"avatar": { "name": "avatar", "type": "text", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"reset_token": { "name": "reset_token", "type": "varchar(255)", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"reset_token_expires": { "name": "reset_token_expires", "type": "timestamp", "primaryKey": false, "notNull": false, "autoincrement": false },
|
||||||
|
"active": { "name": "active", "type": "boolean", "primaryKey": false, "notNull": true, "autoincrement": false, "default": true },
|
||||||
|
"created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, "default": "(now())" },
|
||||||
|
"updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, "notNull": true, "autoincrement": false, "onUpdate": true, "default": "(now())" }
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": { "name": "users_id", "columns": ["id"] }
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": { "name": "users_email_unique", "columns": ["email"] }
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": { "schemas": {}, "tables": {}, "columns": {} },
|
||||||
|
"internal": { "tables": {}, "indexes": {} }
|
||||||
|
}
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "80c783b0-5942-4747-b8da-78c937b01d64",
|
||||||
|
"prevId": "8a064b1a-675e-484f-8a47-d4c6ac4445b7",
|
||||||
|
"tables": {
|
||||||
|
"comment_votes": {
|
||||||
|
"name": "comment_votes",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"comment_id": {
|
||||||
|
"name": "comment_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"vote": {
|
||||||
|
"name": "vote",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_user_comment": {
|
||||||
|
"name": "idx_user_comment",
|
||||||
|
"columns": [
|
||||||
|
"user_id",
|
||||||
|
"comment_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comment_votes_id": {
|
||||||
|
"name": "comment_votes_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"comments": {
|
||||||
|
"name": "comments",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"movie_name": {
|
||||||
|
"name": "movie_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"pinned": {
|
||||||
|
"name": "pinned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"spoiler": {
|
||||||
|
"name": "spoiler",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"anonymous": {
|
||||||
|
"name": "anonymous",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"like_count": {
|
||||||
|
"name": "like_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"dislike_count": {
|
||||||
|
"name": "dislike_count",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"idx_source_slug": {
|
||||||
|
"name": "idx_source_slug",
|
||||||
|
"columns": [
|
||||||
|
"source",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_user_id": {
|
||||||
|
"name": "idx_user_id",
|
||||||
|
"columns": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
},
|
||||||
|
"idx_parent_id": {
|
||||||
|
"name": "idx_parent_id",
|
||||||
|
"columns": [
|
||||||
|
"parent_id"
|
||||||
|
],
|
||||||
|
"isUnique": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"comments_id": {
|
||||||
|
"name": "comments_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"movies": {
|
||||||
|
"name": "movies",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"name": "source",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"origin_name": {
|
||||||
|
"name": "origin_name",
|
||||||
|
"type": "varchar(500)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"thumb": {
|
||||||
|
"name": "thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"name": "poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"year": {
|
||||||
|
"name": "year",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"name": "time",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode": {
|
||||||
|
"name": "episode",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"episode_total": {
|
||||||
|
"name": "episode_total",
|
||||||
|
"type": "varchar(100)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"name": "quality",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lang": {
|
||||||
|
"name": "lang",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"name": "rating",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"name": "views",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"name": "categories",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"countries": {
|
||||||
|
"name": "countries",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"name": "sources",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_poster": {
|
||||||
|
"name": "custom_poster",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_thumb": {
|
||||||
|
"name": "custom_thumb",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_content": {
|
||||||
|
"name": "custom_content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"custom_episodes": {
|
||||||
|
"name": "custom_episodes",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"api_updated_at": {
|
||||||
|
"name": "api_updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"synced_at": {
|
||||||
|
"name": "synced_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"movies_id": {
|
||||||
|
"name": "movies_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraint": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "int",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "varchar(50)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'user'"
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token": {
|
||||||
|
"name": "reset_token",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"reset_token_expires": {
|
||||||
|
"name": "reset_token_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"active": {
|
||||||
|
"name": "active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "(now())"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"onUpdate": true,
|
||||||
|
"default": "(now())"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"users_id": {
|
||||||
|
"name": "users_id",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"checkConstraint": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"tables": {},
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,34 @@
|
|||||||
"when": 1784816479838,
|
"when": 1784816479838,
|
||||||
"tag": "0007_shiny_jane_foster",
|
"tag": "0007_shiny_jane_foster",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1784820307015,
|
||||||
|
"tag": "0008_elite_butterfly",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1784821022237,
|
||||||
|
"tag": "0009_clever_gamora",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1753315200000,
|
||||||
|
"tag": "0010_add_comment_pinned_spoiler",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1784853783915,
|
||||||
|
"tag": "0011_magenta_jack_flag",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { mysqlTable, varchar, text, int, boolean, timestamp, json } from 'drizzle-orm/mysql-core'
|
import { mysqlTable, varchar, text, int, boolean, timestamp, json, index } from 'drizzle-orm/mysql-core'
|
||||||
|
|
||||||
export const users = mysqlTable('users', {
|
export const users = mysqlTable('users', {
|
||||||
id: int('id').primaryKey().autoincrement(),
|
id: int('id').primaryKey().autoincrement(),
|
||||||
@@ -46,7 +46,42 @@ export const movies = mysqlTable('movies', {
|
|||||||
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
|
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const comments = mysqlTable('comments', {
|
||||||
|
id: int('id').primaryKey().autoincrement(),
|
||||||
|
userId: int('user_id').notNull(),
|
||||||
|
source: varchar('source', { length: 50 }).notNull().default(''),
|
||||||
|
slug: varchar('slug', { length: 500 }).notNull(),
|
||||||
|
movieName: varchar('movie_name', { length: 500 }),
|
||||||
|
content: text('content').notNull(),
|
||||||
|
parentId: int('parent_id'),
|
||||||
|
pinned: boolean('pinned').notNull().default(false),
|
||||||
|
spoiler: boolean('spoiler').notNull().default(false),
|
||||||
|
anonymous: boolean('anonymous').notNull().default(false),
|
||||||
|
likeCount: int('like_count').notNull().default(0),
|
||||||
|
dislikeCount: int('dislike_count').notNull().default(0),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
idxSourceSlug: index('idx_source_slug').on(table.source, table.slug),
|
||||||
|
idxUserId: index('idx_user_id').on(table.userId),
|
||||||
|
idxParentId: index('idx_parent_id').on(table.parentId),
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const commentVotes = mysqlTable('comment_votes', {
|
||||||
|
id: int('id').primaryKey().autoincrement(),
|
||||||
|
userId: int('user_id').notNull(),
|
||||||
|
commentId: int('comment_id').notNull(),
|
||||||
|
vote: int('vote').notNull(),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
}, (table) => ({
|
||||||
|
idxUserComment: index('idx_user_comment').on(table.userId, table.commentId),
|
||||||
|
}))
|
||||||
|
|
||||||
export type User = typeof users.$inferSelect
|
export type User = typeof users.$inferSelect
|
||||||
export type NewUser = typeof users.$inferInsert
|
export type NewUser = typeof users.$inferInsert
|
||||||
export type Movie = typeof movies.$inferSelect
|
export type Movie = typeof movies.$inferSelect
|
||||||
export type NewMovie = typeof movies.$inferInsert
|
export type NewMovie = typeof movies.$inferInsert
|
||||||
|
export type Comment = typeof comments.$inferSelect
|
||||||
|
export type NewComment = typeof comments.$inferInsert
|
||||||
|
export type CommentVote = typeof commentVotes.$inferSelect
|
||||||
|
export type NewCommentVote = typeof commentVotes.$inferInsert
|
||||||
|
|||||||
Reference in New Issue
Block a user