"use server" import { prisma } from "@/lib/prisma" import { z } from "zod" /** * Standard action result type */ export type ActionResult = { success: boolean data?: T message?: string errors?: Record } // ============================================================================ // VALIDATION SCHEMAS // ============================================================================ const searchHanziSchema = z.object({ query: z.string().min(1, "Search query is required"), hskLevel: z.string().optional(), limit: z.number().int().positive().max(100).default(20), offset: z.number().int().min(0).default(0), }) const getHanziSchema = z.object({ id: z.string().min(1, "Hanzi ID is required"), }) const getHanziBySimplifiedSchema = z.object({ char: z.string().min(1, "Character is required"), }) // ============================================================================ // HANZI SEARCH // ============================================================================ /** * Search hanzi database (public - no authentication required) * Searches by simplified character, pinyin, or meaning */ export async function searchHanzi( query: string, hskLevel?: string, limit: number = 20, offset: number = 0 ): Promise< ActionResult<{ hanzi: Array<{ id: string simplified: string traditional: string | null pinyin: string | null meaning: string | null hskLevels: string[] radical: string | null frequency: number | null }> total: number hasMore: boolean }> > { try { const validation = searchHanziSchema.safeParse({ query, hskLevel, limit, offset }) if (!validation.success) { return { success: false, message: "Validation failed", errors: validation.error.flatten().fieldErrors, } } // Build where clause const whereClause: any = { OR: [ { simplified: { contains: query, mode: "insensitive" } }, { forms: { some: { OR: [ { traditional: { contains: query, mode: "insensitive" } }, { transcriptions: { some: { value: { contains: query, mode: "insensitive" }, }, }, }, { meanings: { some: { meaning: { contains: query, mode: "insensitive" }, }, }, }, ], }, }, }, ], } // Add HSK level filter if provided if (hskLevel) { whereClause.hskLevels = { some: { level: hskLevel, }, } } // Get total count for pagination const total = await prisma.hanzi.count({ where: whereClause }) // Get hanzi with extra for hasMore check const results = await prisma.hanzi.findMany({ where: whereClause, include: { forms: { where: { isDefault: true }, include: { transcriptions: { where: { type: "pinyin" }, take: 1, }, meanings: { orderBy: { orderIndex: "asc" }, take: 1, }, }, take: 1, }, hskLevels: { select: { level: true }, }, }, skip: offset, take: limit + 1, // Take one extra to check hasMore orderBy: [{ frequency: "asc" }, { simplified: "asc" }], }) const hasMore = results.length > limit const hanziList = results.slice(0, limit) const hanzi = hanziList.map((h) => ({ id: h.id, simplified: h.simplified, traditional: h.forms[0]?.traditional || null, pinyin: h.forms[0]?.transcriptions[0]?.value || null, meaning: h.forms[0]?.meanings[0]?.meaning || null, hskLevels: h.hskLevels.map((l) => l.level), radical: h.radical, frequency: h.frequency, })) return { success: true, data: { hanzi, total, hasMore, }, } } catch (error) { console.error("Failed to search hanzi:", error) return { success: false, message: "Failed to search hanzi", } } } /** * Get detailed hanzi information (public - no authentication required) * Returns all forms, transcriptions, meanings, HSK levels, etc. */ export async function getHanzi(id: string): Promise< ActionResult<{ id: string simplified: string radical: string | null frequency: number | null forms: Array<{ id: string traditional: string isDefault: boolean transcriptions: Array<{ type: string value: string }> meanings: Array<{ language: string meaning: string orderIndex: number }> classifiers: Array<{ classifier: string }> }> hskLevels: Array<{ level: string }> partsOfSpeech: Array<{ pos: string }> }> > { try { const validation = getHanziSchema.safeParse({ id }) if (!validation.success) { return { success: false, message: "Validation failed", errors: validation.error.flatten().fieldErrors, } } const hanzi = await prisma.hanzi.findUnique({ where: { id }, include: { forms: { include: { transcriptions: { orderBy: { type: "asc" }, }, meanings: { include: { language: true, }, orderBy: { orderIndex: "asc" }, }, classifiers: true, }, orderBy: { isDefault: "desc" }, }, hskLevels: true, partsOfSpeech: true, }, }) if (!hanzi) { return { success: false, message: "Hanzi not found", } } const data = { id: hanzi.id, simplified: hanzi.simplified, radical: hanzi.radical, frequency: hanzi.frequency, forms: hanzi.forms.map((form) => ({ id: form.id, traditional: form.traditional, isDefault: form.isDefault, transcriptions: form.transcriptions.map((t) => ({ type: t.type, value: t.value, })), meanings: form.meanings.map((m) => ({ language: m.language.code, meaning: m.meaning, orderIndex: m.orderIndex, })), classifiers: form.classifiers.map((c) => ({ classifier: c.classifier, })), })), hskLevels: hanzi.hskLevels.map((l) => ({ level: l.level, })), partsOfSpeech: hanzi.partsOfSpeech.map((p) => ({ pos: p.pos, })), } return { success: true, data, } } catch (error) { console.error("Failed to get hanzi:", error) return { success: false, message: "Failed to get hanzi details", } } } /** * Get hanzi by simplified character (public - no authentication required) * Quick lookup by simplified character */ export async function getHanziBySimplified(char: string): Promise< ActionResult<{ id: string simplified: string traditional: string | null pinyin: string | null meaning: string | null hskLevels: string[] }> > { try { const validation = getHanziBySimplifiedSchema.safeParse({ char }) if (!validation.success) { return { success: false, message: "Validation failed", errors: validation.error.flatten().fieldErrors, } } const hanzi = await prisma.hanzi.findUnique({ where: { simplified: char }, include: { forms: { where: { isDefault: true }, include: { transcriptions: { where: { type: "pinyin" }, take: 1, }, meanings: { orderBy: { orderIndex: "asc" }, take: 1, }, }, take: 1, }, hskLevels: { select: { level: true }, }, }, }) if (!hanzi) { return { success: false, message: "Hanzi not found", } } const data = { id: hanzi.id, simplified: hanzi.simplified, traditional: hanzi.forms[0]?.traditional || null, pinyin: hanzi.forms[0]?.transcriptions[0]?.value || null, meaning: hanzi.forms[0]?.meanings[0]?.meaning || null, hskLevels: hanzi.hskLevels.map((l) => l.level), } return { success: true, data, } } catch (error) { console.error("Failed to get hanzi by simplified:", error) return { success: false, message: "Failed to get hanzi", } } }