learning randomization
This commit is contained in:
813
src/actions/learning.ts
Normal file
813
src/actions/learning.ts
Normal file
@@ -0,0 +1,813 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { Difficulty } from "@prisma/client"
|
||||
import {
|
||||
selectCardsForSession,
|
||||
calculateCorrectAnswer,
|
||||
calculateIncorrectAnswer,
|
||||
generateWrongAnswers,
|
||||
shuffleOptions,
|
||||
INITIAL_PROGRESS,
|
||||
type SelectableCard,
|
||||
type HanziOption,
|
||||
} from "@/lib/learning/sm2"
|
||||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Standard action result type
|
||||
*/
|
||||
export type ActionResult<T = void> = {
|
||||
success: boolean
|
||||
data?: T
|
||||
message?: string
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VALIDATION SCHEMAS
|
||||
// ============================================================================
|
||||
|
||||
const startLearningSessionSchema = z.object({
|
||||
collectionId: z.string().optional(),
|
||||
cardsCount: z.number().int().positive().default(20),
|
||||
})
|
||||
|
||||
const submitAnswerSchema = z.object({
|
||||
sessionId: z.string().min(1),
|
||||
hanziId: z.string().min(1),
|
||||
selectedPinyin: z.string().min(1),
|
||||
correct: z.boolean(),
|
||||
timeSpentMs: z.number().int().min(0),
|
||||
})
|
||||
|
||||
const endSessionSchema = z.object({
|
||||
sessionId: z.string().min(1),
|
||||
})
|
||||
|
||||
const updateCardDifficultySchema = z.object({
|
||||
hanziId: z.string().min(1),
|
||||
difficulty: z.nativeEnum(Difficulty),
|
||||
})
|
||||
|
||||
const removeFromLearningSchema = z.object({
|
||||
hanziId: z.string().min(1),
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// LEARNING ACTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Start a learning session
|
||||
*
|
||||
* Selects due cards using SM-2 algorithm and creates a session.
|
||||
* If no collectionId provided, selects from all user's collections.
|
||||
*
|
||||
* @param collectionId - Optional collection to learn from
|
||||
* @param cardsCount - Number of cards to include (default: user preference)
|
||||
* @returns Session with cards and answer options
|
||||
*/
|
||||
export async function startLearningSession(
|
||||
collectionId?: string,
|
||||
cardsCount?: number
|
||||
): Promise<ActionResult<{
|
||||
sessionId: string
|
||||
cards: Array<{
|
||||
hanziId: string
|
||||
simplified: string
|
||||
options: string[]
|
||||
correctPinyin: string
|
||||
meaning: string
|
||||
}>
|
||||
}>> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
// Get user preferences
|
||||
const preferences = await prisma.userPreference.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
})
|
||||
|
||||
const cardsPerSession = cardsCount || preferences?.cardsPerSession || 20
|
||||
|
||||
const validation = startLearningSessionSchema.safeParse({
|
||||
collectionId,
|
||||
cardsCount: cardsPerSession,
|
||||
})
|
||||
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: validation.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Get user's hanzi progress for due cards
|
||||
const whereClause: any = {
|
||||
userId: session.user.id,
|
||||
}
|
||||
|
||||
// If collectionId provided, filter by collection
|
||||
if (collectionId) {
|
||||
whereClause.hanzi = {
|
||||
collectionItems: {
|
||||
some: {
|
||||
collectionId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const userProgress = await prisma.userHanziProgress.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
hanzi: {
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: {
|
||||
where: { type: "pinyin" },
|
||||
},
|
||||
},
|
||||
},
|
||||
hskLevels: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Convert to SelectableCard format for SM-2 algorithm
|
||||
const selectableCards: SelectableCard[] = userProgress.map(progress => ({
|
||||
id: progress.hanziId,
|
||||
nextReviewDate: progress.nextReviewDate,
|
||||
incorrectCount: progress.incorrectCount,
|
||||
consecutiveCorrect: progress.consecutiveCorrect,
|
||||
manualDifficulty: progress.manualDifficulty as Difficulty,
|
||||
}))
|
||||
|
||||
// Select cards using SM-2 algorithm
|
||||
const selectedCards = selectCardsForSession(selectableCards, cardsPerSession)
|
||||
|
||||
// If not enough due cards, add new cards from collection
|
||||
if (selectedCards.length < cardsPerSession) {
|
||||
const neededCards = cardsPerSession - selectedCards.length
|
||||
|
||||
// Find hanzi not yet in user progress
|
||||
const newHanziWhereClause: any = {
|
||||
id: {
|
||||
notIn: userProgress.map(p => p.hanziId),
|
||||
},
|
||||
}
|
||||
|
||||
if (collectionId) {
|
||||
newHanziWhereClause.collectionItems = {
|
||||
some: {
|
||||
collectionId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const newHanzi = await prisma.hanzi.findMany({
|
||||
where: newHanziWhereClause,
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: {
|
||||
where: { type: "pinyin" },
|
||||
},
|
||||
},
|
||||
},
|
||||
hskLevels: true,
|
||||
},
|
||||
take: neededCards,
|
||||
})
|
||||
|
||||
// Create initial progress for new cards
|
||||
const now = new Date()
|
||||
for (const hanzi of newHanzi) {
|
||||
await prisma.userHanziProgress.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
hanziId: hanzi.id,
|
||||
...INITIAL_PROGRESS,
|
||||
nextReviewDate: now,
|
||||
},
|
||||
})
|
||||
|
||||
selectedCards.push({
|
||||
id: hanzi.id,
|
||||
nextReviewDate: now,
|
||||
incorrectCount: 0,
|
||||
consecutiveCorrect: 0,
|
||||
manualDifficulty: Difficulty.MEDIUM,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedCards.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "No cards available to learn",
|
||||
}
|
||||
}
|
||||
|
||||
// Create learning session
|
||||
const learningSession = await prisma.learningSession.create({
|
||||
data: {
|
||||
userId: session.user.id,
|
||||
collectionId: collectionId || null,
|
||||
cardsReviewed: selectedCards.length,
|
||||
},
|
||||
})
|
||||
|
||||
// Get full hanzi details for selected cards
|
||||
const selectedHanziIds = selectedCards.map(c => c.id)
|
||||
const hanziDetails = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
id: { in: selectedHanziIds },
|
||||
},
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: {
|
||||
where: { type: "pinyin" },
|
||||
},
|
||||
meanings: true,
|
||||
},
|
||||
},
|
||||
hskLevels: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Generate answer options for each card
|
||||
const cards = []
|
||||
for (const hanzi of hanziDetails) {
|
||||
const defaultForm = hanzi.forms[0]
|
||||
if (!defaultForm) continue
|
||||
|
||||
const pinyinTranscription = defaultForm.transcriptions[0]
|
||||
if (!pinyinTranscription) continue
|
||||
|
||||
const correctPinyin = pinyinTranscription.value
|
||||
|
||||
// Get HSK level for this hanzi
|
||||
const hskLevel = hanzi.hskLevels[0]?.level || "new-1"
|
||||
|
||||
// Get other hanzi from same HSK level for wrong answers
|
||||
const sameHskHanzi = await prisma.hanzi.findMany({
|
||||
where: {
|
||||
id: { not: hanzi.id },
|
||||
hskLevels: {
|
||||
some: {
|
||||
level: hskLevel,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
forms: {
|
||||
where: { isDefault: true },
|
||||
include: {
|
||||
transcriptions: {
|
||||
where: { type: "pinyin" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 10, // Get extra to ensure enough unique options
|
||||
})
|
||||
|
||||
// Convert to HanziOption format
|
||||
const hanziOptions: HanziOption[] = sameHskHanzi
|
||||
.map(h => {
|
||||
const form = h.forms[0]
|
||||
const pinyin = form?.transcriptions[0]
|
||||
if (!form || !pinyin) return null
|
||||
return {
|
||||
id: h.id,
|
||||
simplified: h.simplified,
|
||||
pinyin: pinyin.value,
|
||||
hskLevel: hskLevel,
|
||||
}
|
||||
})
|
||||
.filter((h): h is HanziOption => h !== null)
|
||||
|
||||
// Generate wrong answers
|
||||
let wrongAnswers: string[] = []
|
||||
try {
|
||||
wrongAnswers = generateWrongAnswers(
|
||||
{
|
||||
id: hanzi.id,
|
||||
simplified: hanzi.simplified,
|
||||
pinyin: correctPinyin,
|
||||
hskLevel,
|
||||
},
|
||||
hanziOptions
|
||||
)
|
||||
} catch (error) {
|
||||
// If not enough options, use random ones
|
||||
wrongAnswers = hanziOptions
|
||||
.slice(0, 3)
|
||||
.map(o => o.pinyin)
|
||||
.filter(p => p !== correctPinyin)
|
||||
|
||||
// Fill with placeholders if still not enough
|
||||
while (wrongAnswers.length < 3) {
|
||||
wrongAnswers.push(`Option ${wrongAnswers.length + 1}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle all options
|
||||
const allOptions = shuffleOptions([correctPinyin, ...wrongAnswers])
|
||||
|
||||
// Get English meaning (first meaning)
|
||||
const meaning = defaultForm.meanings[0]?.meaning || ""
|
||||
|
||||
cards.push({
|
||||
hanziId: hanzi.id,
|
||||
simplified: hanzi.simplified,
|
||||
options: allOptions,
|
||||
correctPinyin,
|
||||
meaning,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
sessionId: learningSession.id,
|
||||
cards,
|
||||
},
|
||||
message: `Learning session started with ${cards.length} cards`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Start learning session error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to start learning session",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit an answer for a card
|
||||
*
|
||||
* Records the answer, updates SM-2 progress, and creates a session review.
|
||||
*
|
||||
* @param sessionId - Current session ID
|
||||
* @param hanziId - Hanzi being reviewed
|
||||
* @param selectedPinyin - User's selected answer
|
||||
* @param correct - Whether answer was correct
|
||||
* @param timeSpentMs - Time spent on this card in milliseconds
|
||||
* @returns Updated progress information
|
||||
*/
|
||||
export async function submitAnswer(
|
||||
sessionId: string,
|
||||
hanziId: string,
|
||||
selectedPinyin: string,
|
||||
correct: boolean,
|
||||
timeSpentMs: number
|
||||
): Promise<ActionResult<{
|
||||
easeFactor: number
|
||||
interval: number
|
||||
consecutiveCorrect: number
|
||||
nextReviewDate: Date
|
||||
}>> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
const validation = submitAnswerSchema.safeParse({
|
||||
sessionId,
|
||||
hanziId,
|
||||
selectedPinyin,
|
||||
correct,
|
||||
timeSpentMs,
|
||||
})
|
||||
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: validation.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify session belongs to user
|
||||
const learningSession = await prisma.learningSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
})
|
||||
|
||||
if (!learningSession || learningSession.userId !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid session",
|
||||
}
|
||||
}
|
||||
|
||||
// Get current progress
|
||||
const progress = await prisma.userHanziProgress.findUnique({
|
||||
where: {
|
||||
userId_hanziId: {
|
||||
userId: session.user.id,
|
||||
hanziId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!progress) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Progress not found",
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate new progress using SM-2 algorithm
|
||||
const reviewDate = new Date()
|
||||
const updatedProgress = correct
|
||||
? calculateCorrectAnswer(
|
||||
{
|
||||
easeFactor: progress.easeFactor,
|
||||
interval: progress.interval,
|
||||
consecutiveCorrect: progress.consecutiveCorrect,
|
||||
incorrectCount: progress.incorrectCount,
|
||||
nextReviewDate: progress.nextReviewDate,
|
||||
},
|
||||
reviewDate
|
||||
)
|
||||
: calculateIncorrectAnswer(
|
||||
{
|
||||
easeFactor: progress.easeFactor,
|
||||
interval: progress.interval,
|
||||
consecutiveCorrect: progress.consecutiveCorrect,
|
||||
incorrectCount: progress.incorrectCount,
|
||||
nextReviewDate: progress.nextReviewDate,
|
||||
},
|
||||
reviewDate
|
||||
)
|
||||
|
||||
// Update progress
|
||||
await prisma.userHanziProgress.update({
|
||||
where: {
|
||||
userId_hanziId: {
|
||||
userId: session.user.id,
|
||||
hanziId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
easeFactor: updatedProgress.easeFactor,
|
||||
interval: updatedProgress.interval,
|
||||
consecutiveCorrect: updatedProgress.consecutiveCorrect,
|
||||
incorrectCount: updatedProgress.incorrectCount,
|
||||
nextReviewDate: updatedProgress.nextReviewDate,
|
||||
},
|
||||
})
|
||||
|
||||
// Create session review record
|
||||
await prisma.sessionReview.create({
|
||||
data: {
|
||||
sessionId,
|
||||
hanziId,
|
||||
isCorrect: correct,
|
||||
responseTime: timeSpentMs,
|
||||
},
|
||||
})
|
||||
|
||||
// Update session counts
|
||||
await prisma.learningSession.update({
|
||||
where: { id: sessionId },
|
||||
data: {
|
||||
correctAnswers: correct
|
||||
? { increment: 1 }
|
||||
: undefined,
|
||||
incorrectAnswers: !correct
|
||||
? { increment: 1 }
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
easeFactor: updatedProgress.easeFactor,
|
||||
interval: updatedProgress.interval,
|
||||
consecutiveCorrect: updatedProgress.consecutiveCorrect,
|
||||
nextReviewDate: updatedProgress.nextReviewDate,
|
||||
},
|
||||
message: correct ? "Correct answer!" : "Incorrect answer",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Submit answer error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to submit answer",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End a learning session
|
||||
*
|
||||
* Marks the session as complete and returns summary statistics.
|
||||
*
|
||||
* @param sessionId - Session to end
|
||||
* @returns Session summary with stats
|
||||
*/
|
||||
export async function endSession(
|
||||
sessionId: string
|
||||
): Promise<ActionResult<{
|
||||
totalCards: number
|
||||
correctCount: number
|
||||
incorrectCount: number
|
||||
accuracyPercent: number
|
||||
durationMinutes: number
|
||||
}>> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
const validation = endSessionSchema.safeParse({ sessionId })
|
||||
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: validation.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify session belongs to user
|
||||
const learningSession = await prisma.learningSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
})
|
||||
|
||||
if (!learningSession || learningSession.userId !== session.user.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Invalid session",
|
||||
}
|
||||
}
|
||||
|
||||
if (learningSession.endedAt) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Session already completed",
|
||||
}
|
||||
}
|
||||
|
||||
// Mark session as complete
|
||||
const endedAt = new Date()
|
||||
await prisma.learningSession.update({
|
||||
where: { id: sessionId },
|
||||
data: { endedAt },
|
||||
})
|
||||
|
||||
// Calculate summary stats
|
||||
const totalCards = learningSession.cardsReviewed
|
||||
const correctCount = learningSession.correctAnswers
|
||||
const incorrectCount = learningSession.incorrectAnswers
|
||||
const accuracyPercent =
|
||||
totalCards > 0 ? Math.round((correctCount / totalCards) * 100) : 0
|
||||
const durationMs = endedAt.getTime() - learningSession.startedAt.getTime()
|
||||
const durationMinutes = Math.round(durationMs / 1000 / 60)
|
||||
|
||||
revalidatePath("/dashboard")
|
||||
revalidatePath("/progress")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalCards,
|
||||
correctCount,
|
||||
incorrectCount,
|
||||
accuracyPercent,
|
||||
durationMinutes,
|
||||
},
|
||||
message: "Session completed successfully",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("End session error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to end session",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of due cards
|
||||
*
|
||||
* Returns counts of cards due now, today, and this week.
|
||||
*
|
||||
* @returns Due card counts
|
||||
*/
|
||||
export async function getDueCards(): Promise<ActionResult<{
|
||||
dueNow: number
|
||||
dueToday: number
|
||||
dueThisWeek: number
|
||||
}>> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const endOfToday = new Date(now)
|
||||
endOfToday.setHours(23, 59, 59, 999)
|
||||
const endOfWeek = new Date(now)
|
||||
endOfWeek.setDate(endOfWeek.getDate() + 7)
|
||||
|
||||
const [dueNow, dueToday, dueThisWeek] = await Promise.all([
|
||||
prisma.userHanziProgress.count({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
nextReviewDate: {
|
||||
lte: now,
|
||||
},
|
||||
manualDifficulty: {
|
||||
not: Difficulty.SUSPENDED,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.userHanziProgress.count({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
nextReviewDate: {
|
||||
lte: endOfToday,
|
||||
},
|
||||
manualDifficulty: {
|
||||
not: Difficulty.SUSPENDED,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.userHanziProgress.count({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
nextReviewDate: {
|
||||
lte: endOfWeek,
|
||||
},
|
||||
manualDifficulty: {
|
||||
not: Difficulty.SUSPENDED,
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
dueNow,
|
||||
dueToday,
|
||||
dueThisWeek,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Get due cards error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to get due cards",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manual difficulty for a card
|
||||
*
|
||||
* Allows user to manually mark cards as EASY, NORMAL, HARD, or SUSPENDED.
|
||||
*
|
||||
* @param hanziId - Hanzi to update
|
||||
* @param difficulty - New difficulty level
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function updateCardDifficulty(
|
||||
hanziId: string,
|
||||
difficulty: Difficulty
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
const validation = updateCardDifficultySchema.safeParse({ hanziId, difficulty })
|
||||
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: validation.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.userHanziProgress.update({
|
||||
where: {
|
||||
userId_hanziId: {
|
||||
userId: session.user.id,
|
||||
hanziId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
manualDifficulty: difficulty,
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath("/learn")
|
||||
revalidatePath("/dashboard")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Card difficulty updated to ${difficulty}`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Update card difficulty error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to update card difficulty",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a card from learning
|
||||
*
|
||||
* Suspends the card so it won't appear in future sessions.
|
||||
*
|
||||
* @param hanziId - Hanzi to remove
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function removeFromLearning(hanziId: string): Promise<ActionResult> {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Authentication required",
|
||||
}
|
||||
}
|
||||
|
||||
const validation = removeFromLearningSchema.safeParse({ hanziId })
|
||||
|
||||
if (!validation.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Validation failed",
|
||||
errors: validation.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.userHanziProgress.update({
|
||||
where: {
|
||||
userId_hanziId: {
|
||||
userId: session.user.id,
|
||||
hanziId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
manualDifficulty: Difficulty.SUSPENDED,
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath("/learn")
|
||||
revalidatePath("/dashboard")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Card removed from learning",
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Remove from learning error:", error)
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to remove card from learning",
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user