Files
memohanzi/src/actions/hanzi.ts
2025-11-21 09:51:16 +01:00

367 lines
8.6 KiB
TypeScript

"use server"
import { prisma } from "@/lib/prisma"
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 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",
}
}
}