
Some checks failed
GBCI / build (push) Failing after 4m39s
- Added FileBrowser component for displaying files and directories. - Introduced FileOperations component for handling file uploads and folder creation. - Created FileTree component to visualize the directory structure. - Developed DriveScreen page to integrate file browsing, operations, and UI controls. - Enhanced file system data structure for realistic representation. - Implemented search, filter, and sort functionalities in the file browser. - Added keyboard shortcuts and improved accessibility features.
917 lines
No EOL
28 KiB
TypeScript
917 lines
No EOL
28 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react';
|
|
import StarterKit from '@tiptap/starter-kit';
|
|
import TextStyle from '@tiptap/extension-text-style';
|
|
import FontFamily from '@tiptap/extension-font-family';
|
|
import Color from '@tiptap/extension-color';
|
|
import Highlight from '@tiptap/extension-highlight';
|
|
import TextAlign from '@tiptap/extension-text-align';
|
|
import Link from '@tiptap/extension-link';
|
|
import Image from '@tiptap/extension-image';
|
|
import Underline from '@tiptap/extension-underline';
|
|
import Table from '@tiptap/extension-table';
|
|
import TableCell from '@tiptap/extension-table-cell';
|
|
import TableHeader from '@tiptap/extension-table-header';
|
|
import TableRow from '@tiptap/extension-table-row';
|
|
import {
|
|
Bold, Italic, Underline as UnderlineIcon, AlignLeft, AlignCenter, AlignRight, AlignJustify,
|
|
Link as LinkIcon, Image as ImageIcon, Save, FileText, Printer, Table as TableIcon,
|
|
Plus, Minus, Trash2, Type, Palette, Highlighter, FileImage, File, Home,
|
|
View, ChevronDown, Search,
|
|
Undo, Redo, Copy, MoreVertical
|
|
} from 'lucide-react';
|
|
|
|
const RibbonTab = ({ label, isActive, onClick, children }) => (
|
|
<div className="ribbon-tab">
|
|
<button
|
|
onClick={onClick}
|
|
className={`ribbon-tab-button ${isActive ? 'active' : ''}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
{isActive && (
|
|
<div className="ribbon-content">
|
|
{children}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const RibbonGroup = ({ title, children }) => (
|
|
<div className="ribbon-group">
|
|
<div className="ribbon-group-content">
|
|
{children}
|
|
</div>
|
|
<div className="ribbon-group-title">{title}</div>
|
|
</div>
|
|
);
|
|
|
|
const RibbonButton = ({ icon: Icon, label, onClick, isActive, size = 'medium', dropdown = false }) => (
|
|
<button
|
|
onClick={onClick}
|
|
className={`ribbon-button ${size} ${isActive ? 'active' : ''}`}
|
|
title={label}
|
|
>
|
|
<div className="ribbon-button-content">
|
|
<Icon size={size === 'large' ? 24 : 16} />
|
|
{size === 'large' && <span className="ribbon-button-label">{label}</span>}
|
|
{dropdown && <ChevronDown size={12} className="dropdown-arrow" />}
|
|
</div>
|
|
</button>
|
|
);
|
|
|
|
export default function RibbonWordClone() {
|
|
const [fileName, setFileName] = useState('Document 1');
|
|
const [fontSize, setFontSize] = useState('12');
|
|
const [fontFamily, setFontFamily] = useState('Calibri');
|
|
const [textColor, setTextColor] = useState('#000000');
|
|
const [highlightColor, setHighlightColor] = useState('#ffff00');
|
|
const [activeTab, setActiveTab] = useState('home');
|
|
const [zoom, setZoom] = useState(100);
|
|
const [pages, setPages] = useState([1]);
|
|
const fileInputRef = useRef(null);
|
|
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
TextStyle,
|
|
FontFamily,
|
|
Color,
|
|
Highlight.configure({ multicolor: true }),
|
|
TextAlign.configure({ types: ['heading', 'paragraph', 'tableCell'] }),
|
|
Link.configure({ openOnClick: false }),
|
|
Image,
|
|
Underline,
|
|
Table.configure({
|
|
resizable: true,
|
|
HTMLAttributes: {
|
|
class: 'editor-table',
|
|
},
|
|
}),
|
|
TableRow,
|
|
TableHeader,
|
|
TableCell,
|
|
],
|
|
content: `
|
|
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${fileName}</h1>
|
|
<p><br></p>
|
|
<p>Start typing your document here...</p>
|
|
<p><br></p>
|
|
`,
|
|
onUpdate: ({ editor }) => {
|
|
// Simulate multiple pages based on content height
|
|
const element = document.querySelector('.ProseMirror');
|
|
if (element) {
|
|
const contentHeight = element.scrollHeight;
|
|
const pageHeight = 1123; // A4 height in pixels at 96 DPI
|
|
const neededPages = Math.max(1, Math.ceil(contentHeight / pageHeight));
|
|
if (neededPages !== pages.length) {
|
|
setPages(Array.from({ length: neededPages }, (_, i) => i + 1));
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
const addImage = () => {
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.click();
|
|
}
|
|
};
|
|
|
|
const handleImageUpload = (e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file && editor) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const imageUrl = event.target?.result;
|
|
editor.chain().focus().setImage({ src: imageUrl }).run();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const saveDocument = () => {
|
|
if (editor) {
|
|
const content = editor.getHTML();
|
|
const blob = new Blob([content], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${fileName}.html`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
};
|
|
|
|
if (!editor) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="word-clone">
|
|
<style jsx global>{`
|
|
:root {
|
|
/* 3DBevel Theme */
|
|
--background: 0 0% 80%;
|
|
--foreground: 0 0% 10%;
|
|
--card: 0 0% 75%;
|
|
--card-foreground: 0 0% 10%;
|
|
--popover: 0 0% 80%;
|
|
--popover-foreground: 0 0% 10%;
|
|
--primary: 210 80% 40%;
|
|
--primary-foreground: 0 0% 80%;
|
|
--secondary: 0 0% 70%;
|
|
--secondary-foreground: 0 0% 10%;
|
|
--muted: 0 0% 65%;
|
|
--muted-foreground: 0 0% 30%;
|
|
--accent: 30 80% 40%;
|
|
--accent-foreground: 0 0% 80%;
|
|
--destructive: 0 85% 60%;
|
|
--destructive-foreground: 0 0% 98%;
|
|
--border: 0 0% 70%;
|
|
--input: 0 0% 70%;
|
|
--ring: 210 80% 40%;
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.word-clone {
|
|
min-height: 100vh;
|
|
background: hsl(var(--background));
|
|
color: hsl(var(--foreground));
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
|
|
/* Title Bar */
|
|
.title-bar {
|
|
background: hsl(var(--primary));
|
|
color: hsl(var(--primary-foreground));
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 2px solid hsl(var(--border));
|
|
}
|
|
|
|
.title-bar h1 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
}
|
|
|
|
.title-controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.title-input {
|
|
background: hsl(var(--input));
|
|
border: 1px solid hsl(var(--border));
|
|
border-radius: var(--radius);
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
color: hsl(var(--foreground));
|
|
}
|
|
|
|
/* Quick Access Toolbar */
|
|
.quick-access {
|
|
background: hsl(var(--card));
|
|
border-bottom: 1px solid hsl(var(--border));
|
|
padding: 4px 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.quick-access-btn {
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: 3px;
|
|
padding: 4px;
|
|
cursor: pointer;
|
|
color: hsl(var(--foreground));
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.quick-access-btn:hover {
|
|
background: hsl(var(--muted));
|
|
border-color: hsl(var(--border));
|
|
}
|
|
|
|
/* Ribbon */
|
|
.ribbon {
|
|
background: hsl(var(--card));
|
|
border-bottom: 2px solid hsl(var(--border));
|
|
}
|
|
|
|
.ribbon-tabs {
|
|
display: flex;
|
|
background: hsl(var(--muted));
|
|
border-bottom: 1px solid hsl(var(--border));
|
|
}
|
|
|
|
.ribbon-tab-button {
|
|
background: transparent;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: hsl(var(--muted-foreground));
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.ribbon-tab-button:hover {
|
|
background: hsl(var(--secondary));
|
|
color: hsl(var(--foreground));
|
|
}
|
|
|
|
.ribbon-tab-button.active {
|
|
background: hsl(var(--card));
|
|
color: hsl(var(--foreground));
|
|
border-bottom-color: hsl(var(--primary));
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ribbon-content {
|
|
display: flex;
|
|
padding: 8px;
|
|
gap: 2px;
|
|
min-height: 80px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.ribbon-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid hsl(var(--border));
|
|
padding-right: 8px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.ribbon-group:last-child {
|
|
border-right: none;
|
|
}
|
|
|
|
.ribbon-group-content {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 2px;
|
|
flex: 1;
|
|
align-items: flex-start;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.ribbon-group-title {
|
|
font-size: 10px;
|
|
color: hsl(var(--muted-foreground));
|
|
text-align: center;
|
|
margin-top: 4px;
|
|
border-top: 1px solid hsl(var(--border));
|
|
padding-top: 2px;
|
|
}
|
|
|
|
.ribbon-button {
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
color: hsl(var(--foreground));
|
|
transition: all 0.2s;
|
|
position: relative;
|
|
}
|
|
|
|
.ribbon-button:hover {
|
|
background: hsl(var(--muted));
|
|
border-color: hsl(var(--border));
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.ribbon-button.active {
|
|
background: hsl(var(--primary));
|
|
color: hsl(var(--primary-foreground));
|
|
border-color: hsl(var(--primary));
|
|
}
|
|
|
|
.ribbon-button.medium {
|
|
padding: 6px;
|
|
min-width: 32px;
|
|
min-height: 32px;
|
|
}
|
|
|
|
.ribbon-button.large {
|
|
padding: 8px;
|
|
min-width: 48px;
|
|
min-height: 48px;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.ribbon-button-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.ribbon-button-label {
|
|
font-size: 10px;
|
|
text-align: center;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.dropdown-arrow {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
right: 2px;
|
|
}
|
|
|
|
/* Format Controls */
|
|
.format-select {
|
|
background: hsl(var(--input));
|
|
border: 1px solid hsl(var(--border));
|
|
border-radius: 3px;
|
|
padding: 4px 6px;
|
|
font-size: 11px;
|
|
color: hsl(var(--foreground));
|
|
margin: 2px;
|
|
}
|
|
|
|
.color-picker-wrapper {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.color-picker {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.color-indicator {
|
|
position: absolute;
|
|
bottom: 2px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 16px;
|
|
height: 3px;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
/* Editor Area */
|
|
.editor-container {
|
|
display: flex;
|
|
flex: 1;
|
|
background: hsl(var(--muted));
|
|
}
|
|
|
|
.editor-sidebar {
|
|
width: 200px;
|
|
background: hsl(var(--card));
|
|
border-right: 1px solid hsl(var(--border));
|
|
padding: 16px;
|
|
}
|
|
|
|
.editor-main {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
max-height: calc(100vh - 200px);
|
|
}
|
|
|
|
.pages-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
transform: scale(${zoom / 100});
|
|
transform-origin: top center;
|
|
}
|
|
|
|
.page {
|
|
width: 210mm;
|
|
min-height: 297mm;
|
|
background: white;
|
|
box-shadow:
|
|
0 0 0 1px hsl(var(--border)),
|
|
0 4px 8px rgba(0,0,0,0.1),
|
|
0 8px 16px rgba(0,0,0,0.05);
|
|
position: relative;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-number {
|
|
position: absolute;
|
|
top: -30px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
font-size: 12px;
|
|
color: hsl(var(--muted-foreground));
|
|
background: hsl(var(--background));
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.page-content {
|
|
padding: 25mm;
|
|
min-height: 247mm;
|
|
}
|
|
|
|
.ProseMirror {
|
|
outline: none;
|
|
min-height: 100%;
|
|
}
|
|
|
|
.ProseMirror img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.ProseMirror a {
|
|
color: hsl(var(--primary));
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Table styles */
|
|
.editor-table {
|
|
border-collapse: collapse;
|
|
margin: 16px 0;
|
|
width: 100%;
|
|
border: 1px solid hsl(var(--border));
|
|
}
|
|
|
|
.editor-table td,
|
|
.editor-table th {
|
|
border: 1px solid hsl(var(--border));
|
|
padding: 8px 12px;
|
|
min-width: 50px;
|
|
position: relative;
|
|
}
|
|
|
|
.editor-table th {
|
|
background: hsl(var(--muted));
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Bubble Menu */
|
|
.bubble-menu {
|
|
background: hsl(var(--card));
|
|
border: 1px solid hsl(var(--border));
|
|
border-radius: var(--radius);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
display: flex;
|
|
padding: 4px;
|
|
gap: 2px;
|
|
}
|
|
|
|
.bubble-menu .ribbon-button {
|
|
min-width: 28px;
|
|
min-height: 28px;
|
|
padding: 4px;
|
|
}
|
|
|
|
/* Status Bar */
|
|
.status-bar {
|
|
background: hsl(var(--card));
|
|
border-top: 1px solid hsl(var(--border));
|
|
padding: 4px 12px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 11px;
|
|
color: hsl(var(--muted-foreground));
|
|
}
|
|
|
|
.zoom-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.zoom-slider {
|
|
width: 100px;
|
|
}
|
|
|
|
@media print {
|
|
.title-bar,
|
|
.quick-access,
|
|
.ribbon,
|
|
.editor-sidebar,
|
|
.status-bar {
|
|
display: none !important;
|
|
}
|
|
|
|
.editor-main {
|
|
padding: 0;
|
|
max-height: none;
|
|
}
|
|
|
|
.pages-container {
|
|
transform: none;
|
|
gap: 0;
|
|
}
|
|
|
|
.page {
|
|
box-shadow: none;
|
|
margin: 0;
|
|
break-after: page;
|
|
}
|
|
|
|
.page-number {
|
|
display: none;
|
|
}
|
|
}
|
|
`}</style>
|
|
|
|
{/* Quick Access Toolbar */}
|
|
<div className="quick-access">
|
|
<button className="quick-access-btn" onClick={() => editor.chain().focus().undo().run()}>
|
|
<Undo size={14} />
|
|
</button>
|
|
<button className="quick-access-btn" onClick={() => editor.chain().focus().redo().run()}>
|
|
<Redo size={14} />
|
|
</button>
|
|
<button className="quick-access-btn" onClick={saveDocument}>
|
|
<Save size={14} />
|
|
</button>
|
|
<div className="title-controls">
|
|
<input
|
|
type="text"
|
|
value={fileName}
|
|
onChange={(e) => setFileName(e.target.value)}
|
|
className="title-input"
|
|
placeholder="Document name"
|
|
/>
|
|
<button className="quick-access-btn" onClick={saveDocument}>
|
|
<Save size={14} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Ribbon */}
|
|
<div className="ribbon">
|
|
<div className="ribbon-tabs">
|
|
<button
|
|
className={`ribbon-tab-button ${activeTab === 'home' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('home')}
|
|
>
|
|
Home
|
|
</button>
|
|
<button
|
|
className={`ribbon-tab-button ${activeTab === 'insert' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('insert')}
|
|
>
|
|
Insert
|
|
</button>
|
|
<button
|
|
className={`ribbon-tab-button ${activeTab === 'layout' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('layout')}
|
|
>
|
|
Layout
|
|
</button>
|
|
<button
|
|
className={`ribbon-tab-button ${activeTab === 'view' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('view')}
|
|
>
|
|
View
|
|
</button>
|
|
</div>
|
|
|
|
{activeTab === 'home' && (
|
|
<div className="ribbon-content">
|
|
<RibbonGroup title="Clipboard">
|
|
<RibbonButton
|
|
icon={Copy}
|
|
label="Copy"
|
|
size="large"
|
|
onClick={() => document.execCommand('copy')} isActive={undefined} />
|
|
<RibbonButton
|
|
icon={Copy}
|
|
label="Paste"
|
|
size="large"
|
|
onClick={() => document.execCommand('paste')} isActive={undefined} />
|
|
<RibbonButton
|
|
icon={Copy}
|
|
label="Cut"
|
|
size="medium"
|
|
onClick={() => document.execCommand('cut')} isActive={undefined} />
|
|
</RibbonGroup>
|
|
|
|
<RibbonGroup title="Font">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
<select
|
|
value={fontFamily}
|
|
onChange={(e) => {
|
|
setFontFamily(e.target.value);
|
|
editor.chain().focus().setFontFamily(e.target.value).run();
|
|
}}
|
|
className="format-select"
|
|
style={{ width: '120px' }}
|
|
>
|
|
<option value="Calibri">Calibri</option>
|
|
<option value="Arial">Arial</option>
|
|
<option value="Times New Roman">Times New Roman</option>
|
|
<option value="Georgia">Georgia</option>
|
|
<option value="Verdana">Verdana</option>
|
|
</select>
|
|
<select
|
|
value={fontSize}
|
|
onChange={(e) => {
|
|
setFontSize(e.target.value);
|
|
editor.chain().focus().setFontSize(e.target.value + 'pt').run();
|
|
}}
|
|
className="format-select"
|
|
style={{ width: '60px' }}
|
|
>
|
|
<option value="8">8</option>
|
|
<option value="9">9</option>
|
|
<option value="10">10</option>
|
|
<option value="11">11</option>
|
|
<option value="12">12</option>
|
|
<option value="14">14</option>
|
|
<option value="16">16</option>
|
|
<option value="18">18</option>
|
|
<option value="20">20</option>
|
|
<option value="24">24</option>
|
|
<option value="28">28</option>
|
|
<option value="36">36</option>
|
|
</select>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '2px' }}>
|
|
<RibbonButton
|
|
icon={Bold}
|
|
label="Bold"
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
isActive={editor.isActive('bold')}
|
|
/>
|
|
<RibbonButton
|
|
icon={Italic}
|
|
label="Italic"
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
isActive={editor.isActive('italic')}
|
|
/>
|
|
<RibbonButton
|
|
icon={UnderlineIcon}
|
|
label="Underline"
|
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
isActive={editor.isActive('underline')}
|
|
/>
|
|
<div className="color-picker-wrapper">
|
|
<RibbonButton icon={Type} label="Text Color" onClick={undefined} isActive={undefined} />
|
|
<input
|
|
type="color"
|
|
value={textColor}
|
|
onChange={(e) => {
|
|
setTextColor(e.target.value);
|
|
editor.chain().focus().setColor(e.target.value).run();
|
|
}}
|
|
className="color-picker"
|
|
/>
|
|
<div
|
|
className="color-indicator"
|
|
style={{ backgroundColor: textColor }}
|
|
/>
|
|
</div>
|
|
<div className="color-picker-wrapper">
|
|
<RibbonButton icon={Highlighter} label="Highlight" onClick={undefined} isActive={undefined} />
|
|
<input
|
|
type="color"
|
|
value={highlightColor}
|
|
onChange={(e) => {
|
|
setHighlightColor(e.target.value);
|
|
editor.chain().focus().setHighlight({ color: e.target.value }).run();
|
|
}}
|
|
className="color-picker"
|
|
/>
|
|
<div
|
|
className="color-indicator"
|
|
style={{ backgroundColor: highlightColor }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</RibbonGroup>
|
|
|
|
<RibbonGroup title="Paragraph">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
<div style={{ display: 'flex', gap: '2px' }}>
|
|
<RibbonButton
|
|
icon={AlignLeft}
|
|
label="Align Left"
|
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
isActive={editor.isActive({ textAlign: 'left' })}
|
|
/>
|
|
<RibbonButton
|
|
icon={AlignCenter}
|
|
label="Center"
|
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
|
isActive={editor.isActive({ textAlign: 'center' })}
|
|
/>
|
|
<RibbonButton
|
|
icon={AlignRight}
|
|
label="Align Right"
|
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
isActive={editor.isActive({ textAlign: 'right' })}
|
|
/>
|
|
<RibbonButton
|
|
icon={AlignJustify}
|
|
label="Justify"
|
|
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
|
isActive={editor.isActive({ textAlign: 'justify' })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</RibbonGroup>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'insert' && (
|
|
<div className="ribbon-content">
|
|
<RibbonGroup title="Illustrations">
|
|
<RibbonButton
|
|
icon={ImageIcon}
|
|
label="Picture"
|
|
size="large"
|
|
onClick={addImage} isActive={undefined} />
|
|
</RibbonGroup>
|
|
|
|
<RibbonGroup title="Tables">
|
|
<RibbonButton
|
|
icon={TableIcon}
|
|
label="Table"
|
|
size="large"
|
|
onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()} isActive={undefined} />
|
|
</RibbonGroup>
|
|
|
|
<RibbonGroup title="Links">
|
|
<RibbonButton
|
|
icon={LinkIcon}
|
|
label="Link"
|
|
size="large"
|
|
onClick={() => {
|
|
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();
|
|
}}
|
|
isActive={editor.isActive('link')}
|
|
/>
|
|
</RibbonGroup>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'view' && (
|
|
<div className="ribbon-content">
|
|
<RibbonGroup title="Zoom">
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
|
<div style={{ fontSize: '14px', fontWeight: '600' }}>{zoom}%</div>
|
|
<input
|
|
type="range"
|
|
min="50"
|
|
max="200"
|
|
value={zoom}
|
|
onChange={(e) => setZoom(parseInt(e.target.value))}
|
|
className="zoom-slider"
|
|
/>
|
|
</div>
|
|
</RibbonGroup>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Editor Area */}
|
|
<div className="editor-container">
|
|
<div className="editor-main">
|
|
<div className="pages-container">
|
|
{pages.map((pageNum, index) => (
|
|
<div key={pageNum} className="page">
|
|
<div className="page-number">Page {pageNum}</div>
|
|
<div className="page-content">
|
|
{index === 0 && <EditorContent editor={editor} />}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bubble Menu */}
|
|
{editor && (
|
|
<BubbleMenu editor={editor}>
|
|
<div className="bubble-menu">
|
|
<RibbonButton
|
|
icon={Bold}
|
|
label="Bold"
|
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
isActive={editor.isActive('bold')}
|
|
/>
|
|
<RibbonButton
|
|
icon={Italic}
|
|
label="Italic"
|
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
isActive={editor.isActive('italic')}
|
|
/>
|
|
<RibbonButton
|
|
icon={UnderlineIcon}
|
|
label="Underline"
|
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
isActive={editor.isActive('underline')}
|
|
/>
|
|
<RibbonButton
|
|
icon={LinkIcon}
|
|
label="Link"
|
|
onClick={() => {
|
|
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();
|
|
}}
|
|
isActive={editor.isActive('link')}
|
|
/>
|
|
</div>
|
|
</BubbleMenu>
|
|
)}
|
|
|
|
{/* Status Bar */}
|
|
<div className="status-bar">
|
|
<div>
|
|
Page {pages.length} of {pages.length} | Words: {editor.storage.characterCount?.words() || 0}
|
|
</div>
|
|
<div className="zoom-controls">
|
|
<button onClick={() => setZoom(Math.max(50, zoom - 10))}>-</button>
|
|
<span>{zoom}%</span>
|
|
<button onClick={() => setZoom(Math.min(200, zoom + 10))}>+</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hidden file input */}
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={handleImageUpload}
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
/>
|
|
</div>
|
|
);
|
|
} |