Add Znakopis feature with document and sentence management
- Implemented Znakopis page with document and sentence CRUD operations - Added backend routes for documents and sentences - Created UI components for sentence editing and display - Added sentence store for state management - Integrated select component for document selection - Updated README with Phase 3 completion status - Removed obsolete fix-colors.md file
This commit is contained in:
265
packages/backend/src/routes/documents.ts
Normal file
265
packages/backend/src/routes/documents.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { prisma } from '../lib/prisma.js';
|
||||
import { isAuthenticated } from '../middleware/auth.js';
|
||||
import { Visibility } from '@prisma/client';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/documents
|
||||
* Get all documents for the authenticated user
|
||||
*/
|
||||
router.get('/', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
|
||||
const documents = await prisma.document.findMany({
|
||||
where: {
|
||||
ownerUserId: userId,
|
||||
},
|
||||
include: {
|
||||
pages: {
|
||||
include: {
|
||||
sentences: {
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: true,
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
pageIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ documents });
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching documents:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch documents', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/documents/:id
|
||||
* Get a single document by ID
|
||||
*/
|
||||
router.get('/:id', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.id;
|
||||
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
include: {
|
||||
pages: {
|
||||
include: {
|
||||
sentences: {
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: {
|
||||
include: {
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
pageIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
res.json({ document });
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching document:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch document', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/documents
|
||||
* Create a new document
|
||||
*/
|
||||
router.post('/', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
const { title, description, visibility = 'PRIVATE' } = req.body;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: 'Title is required' });
|
||||
}
|
||||
|
||||
const document = await prisma.document.create({
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
visibility: visibility as Visibility,
|
||||
ownerUserId: userId,
|
||||
pages: {
|
||||
create: {
|
||||
pageIndex: 0,
|
||||
title: 'Stranica 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
pages: {
|
||||
include: {
|
||||
sentences: {
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: true,
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
pageIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ document });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating document:', error);
|
||||
res.status(500).json({ error: 'Failed to create document', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/documents/:id
|
||||
* Update a document
|
||||
*/
|
||||
router.patch('/:id', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.id;
|
||||
const { title, description, visibility } = req.body;
|
||||
|
||||
// Check if document exists and belongs to user
|
||||
const existingDoc = await prisma.document.findFirst({
|
||||
where: {
|
||||
id,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingDoc) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
const document = await prisma.document.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(title && { title }),
|
||||
...(description !== undefined && { description }),
|
||||
...(visibility && { visibility: visibility as Visibility }),
|
||||
},
|
||||
include: {
|
||||
pages: {
|
||||
include: {
|
||||
sentences: {
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: true,
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
pageIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ document });
|
||||
} catch (error: any) {
|
||||
console.error('Error updating document:', error);
|
||||
res.status(500).json({ error: 'Failed to update document', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/documents/:id
|
||||
* Delete a document
|
||||
*/
|
||||
router.delete('/:id', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const userId = req.user?.id;
|
||||
|
||||
// Check if document exists and belongs to user
|
||||
const existingDoc = await prisma.document.findFirst({
|
||||
where: {
|
||||
id,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingDoc) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
await prisma.document.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
res.json({ message: 'Document deleted successfully' });
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting document:', error);
|
||||
res.status(500).json({ error: 'Failed to delete document', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
268
packages/backend/src/routes/sentences.ts
Normal file
268
packages/backend/src/routes/sentences.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { prisma } from '../lib/prisma.js';
|
||||
import { isAuthenticated } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* POST /api/documents/:documentId/pages/:pageIndex/sentences
|
||||
* Create a new sentence on a page
|
||||
*/
|
||||
router.post('/:documentId/pages/:pageIndex/sentences', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { documentId, pageIndex } = req.params;
|
||||
const userId = req.user?.id;
|
||||
const { tokens } = req.body;
|
||||
|
||||
// Verify document ownership
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
// Get or create page
|
||||
let page = await prisma.documentPage.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
pageIndex: parseInt(pageIndex, 10),
|
||||
},
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
page = await prisma.documentPage.create({
|
||||
data: {
|
||||
documentId,
|
||||
pageIndex: parseInt(pageIndex, 10),
|
||||
title: `Stranica ${parseInt(pageIndex, 10) + 1}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Get next sentence index
|
||||
const lastSentence = await prisma.sentence.findFirst({
|
||||
where: {
|
||||
documentPageId: page.id,
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
const nextSentenceIndex = lastSentence ? lastSentence.sentenceIndex + 1 : 0;
|
||||
|
||||
// Create sentence with tokens
|
||||
const sentence = await prisma.sentence.create({
|
||||
data: {
|
||||
documentPageId: page.id,
|
||||
sentenceIndex: nextSentenceIndex,
|
||||
tokens: {
|
||||
create: tokens.map((token: any, index: number) => ({
|
||||
tokenIndex: index,
|
||||
termId: token.termId,
|
||||
displayText: token.displayText,
|
||||
isPunctuation: token.isPunctuation || false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: {
|
||||
include: {
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ sentence });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating sentence:', error);
|
||||
res.status(500).json({ error: 'Failed to create sentence', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/sentences/:sentenceId/tokens
|
||||
* Update tokens in a sentence (reorder, add, remove)
|
||||
*/
|
||||
router.patch('/:sentenceId/tokens', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sentenceId } = req.params;
|
||||
const userId = req.user?.id;
|
||||
const { tokens } = req.body;
|
||||
|
||||
// Verify sentence ownership through document
|
||||
const sentence = await prisma.sentence.findUnique({
|
||||
where: { id: sentenceId },
|
||||
include: {
|
||||
documentPage: {
|
||||
include: {
|
||||
document: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!sentence || sentence.documentPage.document.ownerUserId !== userId) {
|
||||
return res.status(404).json({ error: 'Sentence not found' });
|
||||
}
|
||||
|
||||
// Delete existing tokens and create new ones
|
||||
await prisma.sentenceToken.deleteMany({
|
||||
where: {
|
||||
sentenceId,
|
||||
},
|
||||
});
|
||||
|
||||
// Create new tokens
|
||||
const updatedSentence = await prisma.sentence.update({
|
||||
where: { id: sentenceId },
|
||||
data: {
|
||||
tokens: {
|
||||
create: tokens.map((token: any, index: number) => ({
|
||||
tokenIndex: index,
|
||||
termId: token.termId,
|
||||
displayText: token.displayText,
|
||||
isPunctuation: token.isPunctuation || false,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: {
|
||||
include: {
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ sentence: updatedSentence });
|
||||
} catch (error: any) {
|
||||
console.error('Error updating sentence tokens:', error);
|
||||
res.status(500).json({ error: 'Failed to update sentence tokens', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/sentences/:sentenceId
|
||||
* Delete a sentence
|
||||
*/
|
||||
router.delete('/:sentenceId', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { sentenceId } = req.params;
|
||||
const userId = req.user?.id;
|
||||
|
||||
// Verify sentence ownership through document
|
||||
const sentence = await prisma.sentence.findUnique({
|
||||
where: { id: sentenceId },
|
||||
include: {
|
||||
documentPage: {
|
||||
include: {
|
||||
document: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!sentence || sentence.documentPage.document.ownerUserId !== userId) {
|
||||
return res.status(404).json({ error: 'Sentence not found' });
|
||||
}
|
||||
|
||||
await prisma.sentence.delete({
|
||||
where: { id: sentenceId },
|
||||
});
|
||||
|
||||
res.json({ message: 'Sentence deleted successfully' });
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting sentence:', error);
|
||||
res.status(500).json({ error: 'Failed to delete sentence', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/documents/:documentId/pages
|
||||
* Create a new page in a document
|
||||
*/
|
||||
router.post('/:documentId/pages', isAuthenticated, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { documentId } = req.params;
|
||||
const userId = req.user?.id;
|
||||
const { title } = req.body;
|
||||
|
||||
// Verify document ownership
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
ownerUserId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return res.status(404).json({ error: 'Document not found' });
|
||||
}
|
||||
|
||||
// Get next page index
|
||||
const lastPage = await prisma.documentPage.findFirst({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
orderBy: {
|
||||
pageIndex: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
const nextPageIndex = lastPage ? lastPage.pageIndex + 1 : 0;
|
||||
|
||||
const page = await prisma.documentPage.create({
|
||||
data: {
|
||||
documentId,
|
||||
pageIndex: nextPageIndex,
|
||||
title: title || `Stranica ${nextPageIndex + 1}`,
|
||||
},
|
||||
include: {
|
||||
sentences: {
|
||||
include: {
|
||||
tokens: {
|
||||
include: {
|
||||
term: true,
|
||||
},
|
||||
orderBy: {
|
||||
tokenIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
sentenceIndex: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.status(201).json({ page });
|
||||
} catch (error: any) {
|
||||
console.error('Error creating page:', error);
|
||||
res.status(500).json({ error: 'Failed to create page', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user