Monorepo Integration: Unified Backend, Frontend & Documentation
- Reorganize project into monorepo structure - backend/app/ - New FastAPI backend (modular with src/) - backend/legacy/ - Legacy database modules (relational & vector) - frontend/ - React text editor application - Add launcher.py for easy full-stack startup - Complete documentation in README.md - Quick start guide - API endpoints reference - Development setup - Troubleshooting - Refactor main.py to 35 lines (app configuration only) - Update .gitignore for full-stack project - Add CHANGELOG.md with version history (v0.1.0-v0.1.1) Structure is now clean and ready for team collaboration.
This commit is contained in:
359
frontend/src/TextEditor.js
Normal file
359
frontend/src/TextEditor.js
Normal file
@@ -0,0 +1,359 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { StarterKit } from '@tiptap/starter-kit';
|
||||
import { Table } from '@tiptap/extension-table';
|
||||
import { TableRow } from '@tiptap/extension-table-row';
|
||||
import { TableCell } from '@tiptap/extension-table-cell';
|
||||
import { TableHeader } from '@tiptap/extension-table-header';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import { Image } from '@tiptap/extension-image';
|
||||
import { Mathematics } from '@tiptap/extension-mathematics';
|
||||
import { Underline } from '@tiptap/extension-underline';
|
||||
import { CharacterCount } from '@tiptap/extension-character-count';
|
||||
import MathField from './MathField';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const TextEditor = () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Table.configure({ resizable: true }),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
HTMLAttributes: { class: 'editor-link' }
|
||||
}),
|
||||
Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: '100%',
|
||||
renderHTML: attributes => ({ width: attributes.width }),
|
||||
parseHTML: element => element.getAttribute('width'),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
Mathematics,
|
||||
CharacterCount,
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'tiptap-editor',
|
||||
style: 'outline: none; padding: 20px; min-height: 500px;',
|
||||
},
|
||||
// Obsługa klawisza Tab
|
||||
handleKeyDown: (view, event) => {
|
||||
if (event.key === 'Tab') {
|
||||
if (editor.isActive('table')) return false; // Pozwalamy domyślnej obsłudze Tab w tabeli
|
||||
event.preventDefault();
|
||||
const { state, dispatch } = view;
|
||||
const { selection, tr } = state;
|
||||
const { $from } = selection; // Wyciągamy pozycję kursora jako objekt ResolvedPos
|
||||
if (event.shiftKey) { // Obsługa Shift+Tab
|
||||
const textBefore = $from.parent.textBetween(0, $from.parentOffset);
|
||||
const match = textBefore.match(/ +$/);
|
||||
if (match) {
|
||||
const spacesFound = match[0].length;
|
||||
const amountToRemove = Math.min(spacesFound, 4);
|
||||
const transaction = tr.delete($from.pos - amountToRemove, $from.pos);
|
||||
dispatch(transaction);
|
||||
}
|
||||
} else {
|
||||
// Dla samego Tab wstawiamy 4 spacje
|
||||
const transaction = tr.insertText(' ');
|
||||
dispatch(transaction);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
content: `<h1>Nowy Dokument</h1><p>Zacznij pisać tutaj...</p>`,
|
||||
});
|
||||
|
||||
// Logika dodawania obrazu
|
||||
const addImage = () => {
|
||||
// Jeżeli kursor jest już na obrazie, pobierz jego aktualne atrybuty
|
||||
const existingAttrs = editor.getAttributes('image');
|
||||
const previousUrl = existingAttrs.src || "";
|
||||
const previousWidth = existingAttrs.width || "100%";
|
||||
// Jeśli obrazek już istnieje, możemy najpierw zapytać o nową szerokość
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
|
||||
// Wczywanie/zmiana rozmiaru obrazu
|
||||
if (previousUrl) {
|
||||
const width = prompt("Szerokość obrazu (np. 50% lub 300px):", previousWidth);
|
||||
editor.chain()
|
||||
.focus()
|
||||
.setImage({
|
||||
src: previousUrl,
|
||||
width: width || '100%'
|
||||
})
|
||||
.run();
|
||||
} else {
|
||||
input.onchange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const width = prompt("Szerokość obrazu (np. 50% lub 300px):");
|
||||
editor.chain()
|
||||
.focus()
|
||||
.setImage({
|
||||
src: e.target.result,
|
||||
width: width || '100%'
|
||||
})
|
||||
.run();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
input.click();}
|
||||
};
|
||||
|
||||
// Logika ustawiania linku
|
||||
const setLink = () => {
|
||||
const previousUrl = editor.getAttributes('link').href;
|
||||
const url = window.prompt('URL:', previousUrl);
|
||||
if (url === null) return;
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
};
|
||||
|
||||
// Logika wstawiania formuły matematycznej
|
||||
const [isMathModalOpen, setMathModalOpen] = useState(false);
|
||||
const [tempLatex, setTempLatex] = useState("");
|
||||
|
||||
const insertMath = () => {
|
||||
const existing = editor.getAttributes('inlineMath').latex || '';
|
||||
setTempLatex(existing);
|
||||
setMathModalOpen(true);
|
||||
};
|
||||
|
||||
const handleMathConfirm = () => {
|
||||
if (editor.isActive('inlineMath')) {
|
||||
editor.chain().focus().updateAttributes('inlineMath', { latex: tempLatex }).run();
|
||||
} else {
|
||||
editor.chain().focus().insertContent({
|
||||
type: 'inlineMath',
|
||||
attrs: { latex: tempLatex }
|
||||
}).run();
|
||||
}
|
||||
setMathModalOpen(false);
|
||||
};
|
||||
|
||||
// Logika dodawania tabeli
|
||||
const addTable = () => {
|
||||
const rows = window.prompt("Liczba wierszy:", "3");
|
||||
if (rows === null) return; // Anulowanie
|
||||
const cols = window.prompt("Liczba kolumn:", "3");
|
||||
if (cols === null) return; // Anulowanie
|
||||
editor.chain().focus().insertTable({
|
||||
rows: parseInt(rows) || 3,
|
||||
cols: parseInt(cols) || 3,
|
||||
withHeaderRow: true
|
||||
}).run();
|
||||
};
|
||||
|
||||
// Logika zapisu dokumentu
|
||||
const handleSave = async () => {
|
||||
const defaultTitle = "Dokument Archivium_" + new Date().toLocaleDateString();
|
||||
const userTitle = window.prompt("Podaj nazwę pliku:", defaultTitle);
|
||||
if (userTitle === null) return; // Anulowanie
|
||||
const finalTitle = userTitle.trim() || defaultTitle;
|
||||
|
||||
const docData = {
|
||||
title: finalTitle,
|
||||
content: editor.getJSON(),
|
||||
};
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:8000/save-document', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(docData),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.status === "success") {
|
||||
alert(`Zapisano pomyślnie jako: ${finalTitle}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Błąd połączenia z modułem zapisu!");
|
||||
}
|
||||
};
|
||||
|
||||
// Logika wczytywania dokumentu
|
||||
const handleLoad = async () => {
|
||||
const fileName = window.prompt("Podaj nazwę dokumentu do wczytania:", "");
|
||||
if (fileName === null) return; // Anulowanie
|
||||
try {
|
||||
let url = 'http://127.0.0.1:8000/load-document';
|
||||
if (fileName.trim() !== "") { // Dodaj nazwę pliku jako parametr zapytania, jeśli została podana
|
||||
url += `?title=${encodeURIComponent(fileName.trim())}`;
|
||||
}
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
if (data.content) {
|
||||
editor.commands.setContent(data.content);
|
||||
alert(`Wczytano: ${data.title || fileName}`);
|
||||
} else {
|
||||
alert("Błąd wczytywania: " + (data.error || "Nie znaleziono pliku o tej nazwie."));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Błąd połączenia z modułem wczytywania!");
|
||||
}
|
||||
};
|
||||
|
||||
// Nie renderuj edytora, jeśli nie jest gotowy
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="tools-container" style={{ border: '1px solid #b3b3b3', borderRadius: '4px', overflow: 'hidden', fontFamily: 'sans-serif', position: 'relative' }}>
|
||||
|
||||
{/* Pasek narzędzi formatowania */}
|
||||
<div className="main-toolbar" style={{ background: '#f8f9fa', borderBottom: '1px solid #ddd', padding: '10px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{/* Grupa nagłówków */}
|
||||
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
|
||||
<button onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 1 }) ? 'bold' : 'normal' }}>H1</button>
|
||||
<button onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 2 }) ? 'bold' : 'normal' }}>H2</button>
|
||||
<button onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} style={{ fontWeight: editor.isActive('heading', { level: 3 }) ? 'bold' : 'normal' }}>H3</button>
|
||||
</div>
|
||||
|
||||
{/* Grupa formatowania tekstu */}
|
||||
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
|
||||
<button onClick={() => editor.chain().focus().toggleBold().run()} style={{ fontWeight: editor.isActive('bold') ? 'bold' : 'normal' }}>B</button>
|
||||
<button onClick={() => editor.chain().focus().toggleItalic().run()} style={{ fontStyle: editor.isActive('italic') ? 'italic' : 'normal' }}>I</button>
|
||||
<button onClick={() => editor.chain().focus().toggleUnderline().run()} style={{ textDecoration: editor.isActive('underline') ? 'underline' : 'none' }}>U</button>
|
||||
<button onClick={() => editor.chain().focus().toggleStrike().run()} style={{ textDecoration: editor.isActive('strike') ? 'line-through' : 'none' }}>S</button>
|
||||
</div>
|
||||
|
||||
{/* Grupa list */}
|
||||
<div className="group" style={{ display: 'flex', gap: '2px', borderRight: '1px solid #ddd', paddingRight: '8px' }}>
|
||||
<button onClick={() => editor.chain().focus().toggleBulletList().run()}>• List</button>
|
||||
<button onClick={() => editor.chain().focus().toggleOrderedList().run()}>1. List</button>
|
||||
</div>
|
||||
|
||||
{/* Grupa wstawiania elementów */}
|
||||
<div className="group" style={{ display: 'flex', gap: '2px' }}>
|
||||
<button onClick={setLink} style={{ color: editor.isActive('link') ? '#2196F3' : 'black' }}>Link</button>
|
||||
<button onClick={addImage}>Image</button>
|
||||
<button onClick={insertMath}>Σ Formula</button>
|
||||
<button onClick={addTable}>Tabela</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pasek narzędzi tabeli */}
|
||||
{editor.isActive('table') && (
|
||||
<div className="table-controls" style={{ background: '#e9ecef', padding: '5px 10px', display: 'flex', gap: '10px', alignItems: 'center', fontSize: '12px' }}>
|
||||
<span>Tabela:</span>
|
||||
<button onClick={() => editor.chain().focus().addColumnAfter().run()}>+Kolumna</button>
|
||||
<button onClick={() => editor.chain().focus().deleteColumn().run()}>-Kolumna</button>
|
||||
<button onClick={() => editor.chain().focus().addRowAfter().run()}>+Wiersz</button>
|
||||
<button onClick={() => editor.chain().focus().deleteRow().run()}>-Wiersz</button>
|
||||
<button onClick={() => editor.chain().focus().mergeCells().run()}>Scal</button>
|
||||
<button onClick={() => editor.chain().focus().splitCell().run()}>Rozdziel</button>
|
||||
<button onClick={() => editor.chain().focus().deleteTable().run()} style={{ color: 'red' }}>Usuń</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Obszar edycji */}
|
||||
<div style={{ background: 'white' }}>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
{/* Pasek statusu */}
|
||||
<div className="status-bar" style={{ background: '#f8f9fa', borderTop: '1px solid #ddd', padding: '10px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
Znaki: {editor.storage.characterCount.characters()} | Słowa: {editor.storage.characterCount.words()}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button onClick={handleLoad} style={{ background: '#f0f0f0', border: '1px solid #ccc', padding: '5px 15px', cursor: 'pointer' }}>Wczytaj</button>
|
||||
<button onClick={handleSave} style={{ background: '#4CAF50', color: 'white', border: 'none', padding: '5px 20px', borderRadius: '3px', cursor: 'pointer' }}>Zapisz</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Okno Modala dla edytora funkcji matematycznych */}
|
||||
{isMathModalOpen && (
|
||||
<>
|
||||
{/* Tło (Overlay) */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
zIndex: 999
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Okno Modala */}
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
background: 'white',
|
||||
padding: '25px',
|
||||
zIndex: 1000,
|
||||
boxShadow: '0 10px 30px rgba(0,0,0,0.5)',
|
||||
borderRadius: '12px',
|
||||
width: '450px'
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginTop: 0, color: '#333' }}>Wizualny Edytor Równań</h3>
|
||||
|
||||
<MathField
|
||||
value={tempLatex}
|
||||
onChange={setTempLatex}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: '25px', display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => setMathModalOpen(false)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '6px',
|
||||
background: 'white'
|
||||
}}
|
||||
>
|
||||
Anuluj
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMathConfirm}
|
||||
style={{
|
||||
background: '#4CAF50',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
padding: '10px 25px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
Wstaw do dokumentu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextEditor;
|
||||
Reference in New Issue
Block a user