
All checks were successful
GBCI / build (push) Successful in 14m45s
- Added a new navigation style with responsive design in client-nav.css. - Created a comprehensive editor style in editor/style.css for better user experience. - Introduced paper style for ProseMirror editor with enhanced text formatting options. - Developed a media player component with waveform visualization and media library in player/page.tsx. - Styled media player controls and sliders for improved usability in player/style.css. - Implemented media type detection for audio, video, and slides. - Added keyboard shortcuts for media control and navigation.
510 lines
No EOL
18 KiB
TypeScript
510 lines
No EOL
18 KiB
TypeScript
|
|
"use client";
|
|
|
|
import History from '@tiptap/extension-history';
|
|
import Bold from '@tiptap/extension-bold'; // Import the extension
|
|
import Italic from '@tiptap/extension-italic'; // Import the extension
|
|
import { useState, useRef } 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 {
|
|
Underline as UnderlineIcon, AlignLeft, AlignCenter, AlignRight, AlignJustify,
|
|
Link as LinkIcon, Image as ImageIcon, Save, Table as TableIcon,
|
|
Type, Highlighter,
|
|
ChevronDown,
|
|
Undo, Redo, Copy
|
|
} from 'lucide-react';
|
|
import './style.css';
|
|
|
|
// @ts-ignore
|
|
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: [
|
|
// @ts-ignore
|
|
StarterKit,
|
|
Bold,
|
|
Italic,
|
|
History,
|
|
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>
|
|
`,
|
|
// @ts-ignore
|
|
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;
|
|
// @ts-ignore
|
|
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">
|
|
|
|
{/* 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>
|
|
);
|
|
} |