367 lines
8.6 KiB
TypeScript
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",
|
|
}
|
|
}
|
|
}
|