luna_ocr / src /components /TextViewer.js
veela4's picture
Add files using upload-large-folder tool
373c769 verified
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FileText, Code, Eye, ArrowLeft, Download, Copy, Check, Maximize2, ZoomIn, ZoomOut, Search } from 'lucide-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { marked } from 'marked';
const TextViewer = () => {
const [text, setText] = useState('');
const [fileName, setFileName] = useState('');
const [viewMode, setViewMode] = useState('text');
const [copied, setCopied] = useState(false);
const [fontSize, setFontSize] = useState(16);
const [searchTerm, setSearchTerm] = useState('');
const [isSearching, setIsSearching] = useState(false);
useEffect(() => {
// Get text from URL params or localStorage
const urlParams = new URLSearchParams(window.location.search);
const textParam = urlParams.get('text');
const fileParam = urlParams.get('file');
if (textParam) {
setText(decodeURIComponent(textParam));
} else {
const savedText = localStorage.getItem('extractedText');
if (savedText) setText(savedText);
}
if (fileParam) {
setFileName(decodeURIComponent(fileParam));
} else {
setFileName(localStorage.getItem('fileName') || 'extracted-text');
}
}, []);
const viewModes = [
{ id: 'text', label: 'Text', icon: <FileText size={18} />, ext: '.txt' },
{ id: 'markdown', label: 'Markdown', icon: <Code size={18} />, ext: '.md' },
{ id: 'html', label: 'Rendered', icon: <Eye size={18} />, ext: '.html' },
{ id: 'json', label: 'JSON', icon: <Code size={18} />, ext: '.json' }
];
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text:', err);
}
};
const handleBack = () => {
window.history.back();
};
const adjustFontSize = (delta) => {
setFontSize(prev => Math.max(12, Math.min(24, prev + delta)));
}; cons
t renderContent = () => {
const baseStyle = { fontSize: `${fontSize}px`, lineHeight: 1.7 };
switch (viewMode) {
case 'text':
return (
<div className="reader-content" style={baseStyle}>
<pre className="text-reader">{text}</pre>
</div>
);
case 'markdown':
return (
<div className="reader-content">
<SyntaxHighlighter
language="markdown"
style={atomDark}
customStyle={{
...baseStyle,
background: 'transparent',
border: 'none',
padding: 0,
margin: 0
}}
>
{text}
</SyntaxHighlighter>
</div>
);
case 'html':
return (
<div
className="reader-content html-reader"
style={baseStyle}
dangerouslySetInnerHTML={{ __html: marked(text) }}
/>
);
case 'json':
const jsonData = {
metadata: {
fileName: fileName || 'extracted-text',
extractedAt: new Date().toISOString(),
characterCount: text.length,
lineCount: text.split('\n').length,
wordCount: text.split(/\s+/).filter(word => word.length > 0).length
},
content: {
rawText: text,
lines: text.split('\n'),
paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
}
};
return (
<div className="reader-content">
<SyntaxHighlighter
language="json"
style={atomDark}
customStyle={{
...baseStyle,
background: 'transparent',
border: 'none',
padding: 0,
margin: 0
}}
>
{JSON.stringify(jsonData, null, 2)}
</SyntaxHighlighter>
</div>
);
default:
return null;
}
}; return
(
<div className="text-viewer-app">
{/* Same background as main app */}
<div className="viewer-background">
<div className="giant-orb-background">
<div className="giant-orb-container">
<div className="gradient-orb orb-1"></div>
<div className="gradient-orb orb-2"></div>
<div className="gradient-orb orb-3"></div>
</div>
</div>
</div>
<div className="viewer-glass-container">
{/* Professional Header */}
<motion.header
className="viewer-header"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="header-left">
<motion.button
className="back-button"
onClick={handleBack}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<ArrowLeft size={18} />
Back
</motion.button>
<div className="document-info">
<h1 className="document-title">{fileName}</h1>
<span className="document-stats">
{text.length} chars • {text.split('\n').length} lines • {text.split(/\s+/).filter(w => w.length > 0).length} words
</span>
</div>
</div>
<div className="header-actions">
<div className="search-container">
<Search size={16} />
<input
type="text"
placeholder="Search in document..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
</div>
<div className="font-controls">
<button onClick={() => adjustFontSize(-2)} className="font-btn">
<ZoomOut size={16} />
</button>
<span className="font-size">{fontSize}px</span>
<button onClick={() => adjustFontSize(2)} className="font-btn">
<ZoomIn size={16} />
</button>
</div>
<motion.button
className="action-btn"
onClick={handleCopy}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<AnimatePresence mode="wait">
{copied ? (
<motion.div key="copied" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<Check size={16} />
</motion.div>
) : (
<motion.div key="copy" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<Copy size={16} />
</motion.div>
)}
</AnimatePresence>
{copied ? 'Copied!' : 'Copy'}
</motion.button>
</div>
</motion.header>
{/* Format Selector */}
<motion.div
className="format-selector"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<div className="format-tabs">
{viewModes.map((mode, index) => (
<motion.button
key={mode.id}
className={`format-tab ${viewMode === mode.id ? 'active' : ''}`}
onClick={() => setViewMode(mode.id)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
>
{mode.icon}
<span className="tab-label">{mode.label}</span>
<span className="tab-ext">{mode.ext}</span>
{viewMode === mode.id && (
<motion.div
className="tab-indicator"
layoutId="viewModeIndicator"
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
</motion.button>
))}
</div>
</motion.div>
{/* Professional Reader */}
<motion.main
className="reader-main"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className="reader-panel">
<AnimatePresence mode="wait">
<motion.div
key={viewMode}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
className="content-wrapper"
>
{renderContent()}
</motion.div>
</AnimatePresence>
</div>
</motion.main>
</div>
<style jsx>{`
.text-viewer-app {
min-height: 100vh;
background: #0f0f23;
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
position: relative;
overflow-x: hidden;
}
.viewer-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
.giant-orb-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -10;
display: flex;
align-items: center;
justify-content: center;
}
.giant-orb-container {
width: 100vmin;
height: 100vmin;
position: relative;
pointer-events: none;
transform: translateY(-5vh);
}
.gradient-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.3;
}
.orb-1 {
top: 10%;
right: 20%;
width: 300px;
height: 300px;
background: linear-gradient(45deg, #4facfe, #8b5cf6);
animation: float1 20s ease-in-out infinite;
}
.orb-2 {
bottom: 20%;
left: 15%;
width: 250px;
height: 250px;
background: linear-gradient(45deg, #f093fb, #f5576c);
animation: float2 25s ease-in-out infinite;
}
.orb-3 {
top: 50%;
left: 50%;
width: 200px;
height: 200px;
background: linear-gradient(45deg, #06b6d4, #10b981);
animation: float3 30s ease-in-out infinite;
}
@keyframes float1 {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
25% { transform: translate(-20px, -30px) rotate(90deg); }
50% { transform: translate(10px, -20px) rotate(180deg); }
75% { transform: translate(-10px, 10px) rotate(270deg); }
}
@keyframes float2 {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
33% { transform: translate(30px, -20px) rotate(120deg); }
66% { transform: translate(-20px, -40px) rotate(240deg); }
}
@keyframes float3 {
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
20% { transform: translate(-65%, -35%) rotate(72deg); }
40% { transform: translate(-35%, -35%) rotate(144deg); }
60% { transform: translate(-35%, -65%) rotate(216deg); }
80% { transform: translate(-65%, -65%) rotate(288deg); }
}
.viewer-glass-container {
backdrop-filter: blur(25px);
-webkit-backdrop-filter: blur(25px);
background: rgba(0, 0, 0, 0.1);
border: none;
border-radius: 16px;
margin: 12px;
min-height: calc(100vh - 24px);
overflow: hidden;
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
}
.viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.back-button {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 16px;
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.back-button:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(79, 172, 254, 0.3);
}
.document-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.document-title {
font-size: 1.25rem;
font-weight: 600;
color: #ffffff;
margin: 0;
}
.document-stats {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.search-container {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 6px 12px;
backdrop-filter: blur(10px);
}
.search-input {
background: none;
border: none;
color: #ffffff;
font-size: 0.85rem;
outline: none;
width: 200px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.font-controls {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 4px 8px;
backdrop-filter: blur(10px);
}
.font-btn {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.3s ease;
}
.font-btn:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.1);
}
.font-size {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.8);
min-width: 32px;
text-align: center;
}
.action-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 8px 16px;
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(79, 172, 254, 0.3);
}
.format-selector {
display: flex;
justify-content: center;
padding: 16px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.format-tabs {
display: flex;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 4px;
backdrop-filter: blur(20px);
}
.format-tab {
background: none;
border: none;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.7);
transition: all 0.3s ease;
position: relative;
min-width: 120px;
justify-content: center;
}
.format-tab:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.05);
}
.format-tab.active {
color: #ffffff;
background: rgba(255, 255, 255, 0.08);
}
.tab-label {
font-weight: 500;
}
.tab-ext {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
font-weight: 400;
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, #4facfe, #8b5cf6);
border-radius: 1px;
}
.reader-main {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.reader-panel {
flex: 1;
overflow: auto;
padding: 0;
background: rgba(255, 255, 255, 0.02);
margin: 16px 24px 24px;
border-radius: 12px;
backdrop-filter: blur(10px);
}
.content-wrapper {
height: 100%;
overflow: auto;
}
.reader-content {
padding: 32px;
max-width: 900px;
margin: 0 auto;
line-height: 1.8;
}
.text-reader {
background: none;
border: none;
color: #ffffff;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
white-space: pre-wrap;
word-wrap: break-word;
width: 100%;
margin: 0;
padding: 0;
line-height: inherit;
} .ht
ml-reader {
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
}
.html-reader h1,
.html-reader h2,
.html-reader h3,
.html-reader h4,
.html-reader h5,
.html-reader h6 {
color: #ffffff;
margin-bottom: 24px;
margin-top: 40px;
font-weight: 600;
line-height: 1.3;
}
.html-reader h1 {
font-size: 2.5rem;
background: linear-gradient(45deg, #4facfe, #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
border-bottom: 2px solid rgba(79, 172, 254, 0.3);
padding-bottom: 16px;
}
.html-reader h2 {
font-size: 2rem;
color: #4facfe;
border-left: 4px solid #4facfe;
padding-left: 16px;
}
.html-reader h3 {
font-size: 1.5rem;
color: #8b5cf6;
}
.html-reader p {
margin-bottom: 20px;
color: rgba(255, 255, 255, 0.9);
text-align: justify;
}
.html-reader strong,
.html-reader b {
color: #ffffff;
font-weight: 700;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.html-reader em,
.html-reader i {
color: rgba(255, 255, 255, 0.95);
font-style: italic;
}
.html-reader ul,
.html-reader ol {
margin-bottom: 20px;
padding-left: 32px;
}
.html-reader li {
margin-bottom: 12px;
color: rgba(255, 255, 255, 0.9);
position: relative;
}
.html-reader ul li::marker {
color: #4facfe;
}
.html-reader ol li::marker {
color: #8b5cf6;
font-weight: 600;
}
.html-reader table {
width: 100%;
border-collapse: collapse;
margin: 32px 0;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.html-reader th,
.html-reader td {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 16px 20px;
text-align: left;
vertical-align: top;
}
.html-reader th {
background: linear-gradient(135deg, rgba(79, 172, 254, 0.2), rgba(139, 92, 246, 0.2));
font-weight: 700;
color: #ffffff;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
}
.html-reader td {
background: rgba(255, 255, 255, 0.02);
}
.html-reader tr:nth-child(even) td {
background: rgba(255, 255, 255, 0.04);
}
.html-reader tr:hover td {
background: rgba(79, 172, 254, 0.05);
}
.html-reader blockquote {
border-left: 4px solid #4facfe;
padding: 20px 24px;
margin: 24px 0;
background: rgba(79, 172, 254, 0.05);
border-radius: 0 8px 8px 0;
font-style: italic;
color: rgba(255, 255, 255, 0.9);
position: relative;
}
.html-reader blockquote::before {
content: '"';
font-size: 4rem;
color: rgba(79, 172, 254, 0.3);
position: absolute;
top: -10px;
left: 10px;
font-family: serif;
}
.html-reader code {
background: rgba(0, 0, 0, 0.4);
padding: 4px 8px;
border-radius: 6px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.9em;
color: #4facfe;
border: 1px solid rgba(79, 172, 254, 0.2);
}
.html-reader pre {
background: rgba(0, 0, 0, 0.4);
padding: 24px;
border-radius: 12px;
overflow-x: auto;
margin: 24px 0;
border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
}
.html-reader pre code {
background: none;
padding: 0;
border: none;
color: rgba(255, 255, 255, 0.9);
}
.html-reader a {
color: #4facfe;
text-decoration: none;
border-bottom: 1px solid rgba(79, 172, 254, 0.3);
transition: all 0.3s ease;
}
.html-reader a:hover {
color: #8b5cf6;
border-bottom-color: rgba(139, 92, 246, 0.5);
text-shadow: 0 0 8px rgba(139, 92, 246, 0.3);
}
.html-reader hr {
border: none;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(79, 172, 254, 0.5), transparent);
margin: 40px 0;
}