DB, Collections, Search
This commit is contained in:
366
src/actions/hanzi.ts
Normal file
366
src/actions/hanzi.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
"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",
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user