Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions client/src/App.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
import { useState } from 'react'
import Editor from './components/Editor'
import Results from './components/Results'
import ReportModal from './components/ReportModal'
import { useAnalysis } from './hooks/useAnalysis'
import { useState } from 'react'
import Editor from './components/Editor'
import ReportModal from './components/ReportModal'
import { useAnalysis } from './hooks/useAnalysis'
import { useTheme } from './hooks/useTheme'

export default function App() {
const { result, loading, error, analyze, exportReport, reset } = useAnalysis()
const [showReport, setShowReport] = useState(false)
const [editorText, setEditorText] = useState('')
const { isDark, toggle } = useTheme()
const [showReport, setShowReport] = useState(false)
const [editorText, setEditorText] = useState('')

const handleAnalyze = async (text, language) => {
setEditorText(text)
return await analyze(text, language)
}

const handleExport = (format) => {
exportReport(format, editorText, result)
exportReport(format, editorText, result)
}

return (
<div style={{ maxWidth: '860px', margin: '40px auto', padding: '0 24px' }}>
<h1 style={{ fontSize: '24px', color: '#1e293b', marginBottom: '24px' }}>
Text Analyzer
</h1>

<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Editor
loading={loading}
issues={result?.issues}
result={result}
onAnalyze={handleAnalyze}
onOpenReport={() => setShowReport(true)}
onClear={reset}
isDark={isDark}
onToggleTheme={toggle}
/>

{error && (
<div style={{
marginTop: '16px',
position: 'fixed',
bottom: '20px',
right: '20px',
padding: '12px 16px',
backgroundColor: '#FEE2E2',
color: '#991B1B',
borderRadius: '8px'
borderRadius: '8px',
zIndex: 999
}}>
{error}
</div>
)}

<Results result={result} />

{showReport && (
<ReportModal
result={result}
text={editorText}
onClose={() => setShowReport(false)}
onExport={handleExport}
/>
<ReportModal
result={result}
text={editorText}
onClose={() => setShowReport(false)}
onExport={handleExport}
/>
)}
</div>
)
Expand Down
172 changes: 99 additions & 73 deletions client/src/components/Editor.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,49 @@
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import TextAlign from '@tiptap/extension-text-align'
import { TextStyleKit } from '@tiptap/extension-text-style'
import { useState, useCallback, useRef } from 'react'

import { CATEGORY_COLORS, LANG_LABELS } from '../constants/categories'
import { HighlightExtension, applyHighlights, clearHighlights, highlightKey } from '../utils/highlight'
import { detectLanguage } from '../hooks/useAnalysis'
import { DecorationSet } from 'prosemirror-view'
import IssuePopup from './IssuePopup'
import Toolbar from './Toolbar'
import { CATEGORY_COLORS, LANG_LABELS } from '../constants/categories'
import { HighlightExtension, applyHighlights, clearHighlights } from '../utils/highlight'
import { detectLanguage } from '../hooks/useAnalysis'
import IssuePopup from './IssuePopup'
import Toolbar from './Toolbar'
import EditorToolbar from './EditorToolbar'

export default function Editor({ onAnalyze, onExport, loading, issues, result, onOpenReport }) {
export default function Editor({ onAnalyze, loading, issues, result, onOpenReport, onClear, isDark, onToggleTheme }) {
const [detectedLang, setDetectedLang] = useState('ru')
const [langTimer, setLangTimer] = useState(null)
const [popup, setPopup] = useState(null)
const [wordCount, setWordCount] = useState(0)
const editorRef = useRef(null)

const editor = useEditor({
extensions: [StarterKit, HighlightExtension],
extensions: [
StarterKit,
Underline,
TextStyleKit,
TextAlign.configure({ types: ['heading', 'paragraph'] }),
HighlightExtension,
],
content: '<p>Введите текст для анализа...</p>',
editorProps: {
attributes: {
spellcheck: 'true',
lang: detectedLang,
style: 'outline: none; min-height: 250px;'
style: 'outline: none; min-height: 500px; padding: 60px; font-size: 14px; line-height: 1.8; font-family: Arial;'
}
},
onUpdate: ({ editor }) => {
const text = editor.getText()
setWordCount(text.trim() ? text.trim().split(/\s+/).length : 0)
if (langTimer) clearTimeout(langTimer)
const timer = setTimeout(() => {
setDetectedLang(detectLanguage(editor.getText()))
}, 1500)
const timer = setTimeout(() => setDetectedLang(detectLanguage(text)), 1500)
setLangTimer(timer)
}
})

// Передаём текст и язык наверх для анализа
const handleAnalyze = useCallback(async () => {
const text = editor?.getText()?.trim()
if (!text) return
Expand All @@ -46,25 +55,18 @@ export default function Editor({ onAnalyze, onExport, loading, issues, result, o
editor?.commands.setContent('<p></p>')
clearHighlights(editor)
setPopup(null)
setWordCount(0)
onClear()
}

const handleExport = (format) => {
const text = editor?.getText()?.trim()
onExport(format, text, detectedLang)
}

// Клик по подсветке
const handleClick = useCallback((e) => {
const span = e.target.closest('.issue-highlight')
if (!span) { setPopup(null); return }

const idx = parseInt(span.dataset.issueIdx)
const issue = issues?.[idx]
if (!issue) return

const rect = span.getBoundingClientRect()
const containerRect = editorRef.current?.getBoundingClientRect()

setPopup({
x: rect.left - (containerRect?.left || 0),
y: rect.bottom - (containerRect?.top || 0) + 8,
Expand All @@ -73,63 +75,87 @@ export default function Editor({ onAnalyze, onExport, loading, issues, result, o
}, [issues])

return (
<div ref={editorRef} style={{ position: 'relative' }}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>

{/* Язык */}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '14px', color: '#64748b' }}>Редактор</span>
{/* Тулбар */}
<EditorToolbar
editor={editor}
wordCount={wordCount}
isDark={isDark}
onToggleTheme={onToggleTheme}
/>

{/* Рабочая область */}
<div style={{
flex: 1,
backgroundColor: isDark ? '#1e1e2e' : '#f0f0f0',
overflowY: 'auto',
padding: '24px 0'
}}>
{/* Лист A4 */}
<div
ref={editorRef}
onClick={handleClick}
style={{
position: 'relative',
width: '794px',
minHeight: '1123px',
margin: '0 auto',
backgroundColor: isDark ? '#2d2d3f' : '#fff',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
color: isDark ? '#e2e8f0' : '#1e293b'
}}
>
<EditorContent editor={editor} />
<IssuePopup popup={popup} onClose={() => setPopup(null)} isDark={isDark} />
</div>
</div>

{/* Нижняя панель */}
<div style={{
backgroundColor: isDark ? '#1a1a2e' : '#fff',
borderTop: `1px solid ${isDark ? '#2d2d3f' : '#e2e8f0'}`,
padding: '8px 24px',
display: 'flex',
alignItems: 'center',
gap: '12px',
flexWrap: 'wrap'
}}>
<span style={{
fontSize: '13px', padding: '2px 10px',
backgroundColor: '#f1f5f9', borderRadius: '999px', color: '#475569'
fontSize: '12px',
padding: '2px 10px',
backgroundColor: '#f1f5f9',
borderRadius: '999px',
color: '#475569'
}}>
{LANG_LABELS[detectedLang]}
</span>
</div>

{/* Редактор */}
<div
onClick={handleClick}
style={{
border: '1.5px solid #e2e8f0',
borderRadius: '10px',
padding: '16px',
backgroundColor: '#fff',
fontSize: '16px',
lineHeight: '1.7',
minHeight: '260px'
}}
>
<EditorContent editor={editor} />
<Toolbar
loading={loading}
hasResult={!!result}
onAnalyze={handleAnalyze}
onClear={handleClear}
onOpenReport={onOpenReport}
isDark={isDark}
/>

{issues?.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginLeft: 'auto' }}>
{[...new Set(issues.map(i => i.category))].map(cat => (
<span key={cat} style={{
padding: '2px 8px',
backgroundColor: CATEGORY_COLORS[cat] || '#e2e8f0',
borderRadius: '999px',
fontSize: '11px',
color: '#1e293b'
}}>
{cat}
</span>
))}
</div>
)}
</div>

{/* Тулбар */}
<Toolbar
loading={loading}
hasResult={!!result}
onAnalyze={handleAnalyze}
onClear={handleClear}
onOpenReport={onOpenReport}
/>

{/* Легенда */}
{issues?.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '12px' }}>
{[...new Set(issues.map(i => i.category))].map(cat => (
<span key={cat} style={{
padding: '2px 10px',
backgroundColor: CATEGORY_COLORS[cat] || '#e2e8f0',
borderRadius: '999px',
fontSize: '12px',
color: '#1e293b'
}}>
{cat}
</span>
))}
</div>
)}

{/* Popup */}
<IssuePopup popup={popup} onClose={() => setPopup(null)} />
</div>
)
}
Loading
Loading