Add dictionary feature with term management and UI components

- Implement backend API for term CRUD operations
- Add frontend dictionary page with search and filtering
- Integrate shadcn/ui components (Dialog)
- Create term management UI with add/edit/delete functionality
- Update database seed with initial terms
- Add API client for term operations
- Complete Phase 2 of development plan
This commit is contained in:
2026-01-17 18:15:01 +01:00
parent bbf143a3b4
commit c6d6c18466
21 changed files with 1757 additions and 176 deletions

View File

@@ -0,0 +1,134 @@
import { Router, Request, Response } from 'express';
import { prisma } from '../lib/prisma.js';
import { WordType, CefrLevel } from '@prisma/client';
const router = Router();
/**
* GET /api/terms
* Get all terms with optional filtering, search, and pagination
* Query params:
* - query: search text (searches wordText and normalizedText)
* - wordType: filter by word type (NOUN, VERB, etc.)
* - cefrLevel: filter by CEFR level (A1, A2, B1, B2, C1, C2)
* - page: page number (default: 1)
* - limit: items per page (default: 20)
*/
router.get('/', async (req: Request, res: Response) => {
try {
const {
query = '',
wordType,
cefrLevel,
page = '1',
limit = '20',
} = req.query;
const pageNum = parseInt(page as string, 10);
const limitNum = parseInt(limit as string, 10);
const skip = (pageNum - 1) * limitNum;
// Build where clause
const where: any = {};
// Add search filter
if (query && typeof query === 'string' && query.trim()) {
where.OR = [
{ wordText: { contains: query.trim() } },
{ normalizedText: { contains: query.trim() } },
];
}
// Add wordType filter
if (wordType && typeof wordType === 'string') {
const validWordTypes = Object.values(WordType);
if (validWordTypes.includes(wordType as WordType)) {
where.wordType = wordType as WordType;
}
}
// Add cefrLevel filter
if (cefrLevel && typeof cefrLevel === 'string') {
const validCefrLevels = Object.values(CefrLevel);
if (validCefrLevels.includes(cefrLevel as CefrLevel)) {
where.cefrLevel = cefrLevel as CefrLevel;
}
}
// Get total count for pagination
const total = await prisma.term.count({ where });
// Get terms with media
const terms = await prisma.term.findMany({
where,
include: {
media: {
orderBy: {
createdAt: 'asc',
},
},
examples: {
orderBy: {
createdAt: 'asc',
},
},
},
orderBy: {
wordText: 'asc',
},
skip,
take: limitNum,
});
res.json({
terms,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
},
});
} catch (error: any) {
console.error('Error fetching terms:', error);
res.status(500).json({ error: 'Failed to fetch terms', message: error.message });
}
});
/**
* GET /api/terms/:id
* Get a single term by ID with all related data
*/
router.get('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const term = await prisma.term.findUnique({
where: { id },
include: {
media: {
orderBy: {
createdAt: 'asc',
},
},
examples: {
orderBy: {
createdAt: 'asc',
},
},
},
});
if (!term) {
return res.status(404).json({ error: 'Term not found' });
}
res.json({ term });
} catch (error: any) {
console.error('Error fetching term:', error);
res.status(500).json({ error: 'Failed to fetch term', message: error.message });
}
});
export default router;