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
3,189 changes: 3,092 additions & 97 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,27 @@
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@blocknote/core": "^0.42.3",
"@blocknote/mantine": "^0.42.3",
"@blocknote/react": "^0.42.3",
"@ckeditor/ckeditor5-build-classic": "^40.0.0",
"@ckeditor/ckeditor5-react": "^6.1.0",
"@reduxjs/toolkit": "^1.9.7",
"antd": "^5.10.0",
"axios": "^1.5.1",
"axios-retry": "^4.0.0",
"chart.js": "^4.5.1",
"date-fns": "^2.30.0",
"dayjs": "^1.11.18",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"react-redux": "^8.1.3",
"react-router-dom": "^6.16.0",
"react-slick": "^0.29.0",
"slick-carousel": "^1.8.1",
"socket.io-client": "^4.7.2",
"styled-components": "^6.1.19",
"zustand": "^4.4.6"
},
"devDependencies": {
Expand Down
220 changes: 220 additions & 0 deletions src/components/BlockNoteEditor/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { useEffect, useMemo, useState, useCallback, useRef } from "react";
import { BlockNoteView } from "@blocknote/mantine";
import { useCreateBlockNote } from "@blocknote/react";
import "@blocknote/core/fonts/inter.css";
import "@blocknote/mantine/style.css";
import { Button } from "antd";
import { FullscreenOutlined, FullscreenExitOutlined } from "@ant-design/icons";
import styled from "styled-components";
import PropTypes from "prop-types";

const BlockNoteEditor = ({ initialContent, onChange, placeholder = "Nhập nội dung bài viết..." }) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const onChangeRef = useRef(onChange);

// Update ref when onChange changes
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);

// Parse initial content
const parsedInitialContent = useMemo(() => {
if (!initialContent) return undefined;

try {
if (typeof initialContent === 'string' && initialContent.trim().startsWith('<')) {
return undefined; // BlockNote will use default blocks
}

if (Array.isArray(initialContent)) {
return initialContent;
}

return undefined;
} catch (error) {
console.error('Error parsing initial content:', error);
return undefined;
}
}, [initialContent]);

// Create editor instance
const editor = useCreateBlockNote({
initialContent: parsedInitialContent,
onChange: () => {
handleEditorChange();
},
Comment on lines +43 to +45
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The editor change handler is called directly in the onChange callback, which triggers on every keystroke. This approach bypasses the debouncing logic defined in the useEffect hook (lines 85-112). Consider removing the direct call here and relying solely on the debounced subscription to avoid performance issues with rapid state updates.

Suggested change
onChange: () => {
handleEditorChange();
},
// Removed direct onChange handler to rely on debounced logic in useEffect

Copilot uses AI. Check for mistakes.
});
Comment on lines +41 to +46
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onChange callback in useCreateBlockNote calls handleEditorChange() but handleEditorChange is defined with useCallback that depends on editor. This creates a circular dependency where the editor's onChange references a function that depends on the editor itself. Consider removing the editor dependency from the useCallback or restructuring the logic to avoid this circular reference.

Suggested change
const editor = useCreateBlockNote({
initialContent: parsedInitialContent,
onChange: () => {
handleEditorChange();
},
});
// Use a ref to hold the latest editor instance
const editorRef = useRef(null);
const editor = useCreateBlockNote({
initialContent: parsedInitialContent,
onChange: () => {
// Use the ref to access the latest editor instance if needed
if (onChangeRef.current) {
// If you need to pass the editor's content, use editorRef.current
// For example: onChangeRef.current(editorRef.current?.getContent());
// If not, just call onChangeRef.current()
onChangeRef.current();
}
},
});
// Update the ref whenever editor changes
useEffect(() => {
editorRef.current = editor;
}, [editor]);

Copilot uses AI. Check for mistakes.

// Load HTML content separately
useEffect(() => {
if (!editor || !initialContent) return;

if (typeof initialContent === 'string' && initialContent.trim().startsWith('<')) {
const loadContent = async () => {
try {
const blocks = editor.tryParseHTMLToBlocks(initialContent);
editor.replaceBlocks(editor.document, blocks);
} catch (error) {
console.error('Error loading HTML content:', error);
}
};
loadContent();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editor]); // Only run once when editor is ready

// Handle content changes
const handleEditorChange = useCallback(async () => {
if (!editor) return;

try {
const blocks = editor.document;
const html = editor.blocksToHTMLLossy(blocks);

// Use setTimeout to defer the state update and avoid blocking UI
setTimeout(() => {
if (onChangeRef.current) {
onChangeRef.current(html, blocks);
}
}, 0);
} catch (error) {
console.error('Error handling editor change:', error);
}
}, [editor]);

useEffect(() => {
if (!editor) return;

let timeoutId = null;

// Debounce the change handler to avoid rapid successive calls
const debouncedHandler = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}

timeoutId = setTimeout(() => {
handleEditorChange();
}, 100);
};

// Subscribe to editor changes
const unsubscribe = editor.onChange(debouncedHandler);

return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (typeof unsubscribe === 'function') {
unsubscribe();
}
};
}, [editor, handleEditorChange]);

Comment on lines +85 to +113
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate change handler subscription. The editor's onChange is already set in useCreateBlockNote (line 43), but then another subscription is created in the useEffect (line 102). This will cause the change handler to fire twice for every edit, which is inefficient and could lead to performance issues or unexpected behavior.

Suggested change
useEffect(() => {
if (!editor) return;
let timeoutId = null;
// Debounce the change handler to avoid rapid successive calls
const debouncedHandler = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
handleEditorChange();
}, 100);
};
// Subscribe to editor changes
const unsubscribe = editor.onChange(debouncedHandler);
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (typeof unsubscribe === 'function') {
unsubscribe();
}
};
}, [editor, handleEditorChange]);
// Removed duplicate editor.onChange subscription useEffect.

Copilot uses AI. Check for mistakes.
// Handle fullscreen
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape' && isFullscreen) {
setIsFullscreen(false);
}
};

if (isFullscreen) {
document.addEventListener('keydown', handleEscape);
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}

return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = '';
};
}, [isFullscreen]);

if (!editor) {
return <div>Loading editor...</div>;
}

return (
<EditorWrapper className={isFullscreen ? 'fullscreen' : ''}>
<EditorHeader>
<h4>{isFullscreen ? 'Chế độ toàn màn hình - Nhấn ESC để thoát' : 'Nội dung bài viết'}</h4>
<Button
type="text"
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={() => setIsFullscreen(!isFullscreen)}
>
{isFullscreen ? 'Thu nhỏ' : 'Toàn màn hình'}
</Button>
</EditorHeader>
<BlockNoteView
editor={editor}
theme="light"
data-theming-css-variables-demo
placeholder={placeholder}
/>
</EditorWrapper>
);
};

BlockNoteEditor.propTypes = {
initialContent: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array
]),
onChange: PropTypes.func,
placeholder: PropTypes.string
};

const EditorWrapper = styled.div`
position: relative;
border: 1px solid #d9d9d9;
border-radius: 8px;
min-height: 400px;

&.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: white;
border-radius: 0;

.bn-container {
height: calc(100vh - 60px);
}
}

.bn-container {
padding: 16px;

.bn-editor {
min-height: 300px;
}
}
`;

const EditorHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #d9d9d9;
background: #fafafa;
border-radius: 8px 8px 0 0;

.fullscreen & {
border-radius: 0;
}

h4 {
margin: 0;
font-size: 14px;
color: #595959;
}
`;

export default BlockNoteEditor;
2 changes: 2 additions & 0 deletions src/components/Column/GenerateColumn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const generateBasicColumn = (
// title: () => <ColumnSort type="id" title="ID" handleSort={handleSort} />,
dataIndex: "id",
key: "id",
width: 200,
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The width property is set to a fixed pixel value (200px) which may not work well on smaller screens or with responsive layouts. Consider using a percentage-based width or making it configurable, or testing this value across different viewport sizes to ensure it doesn't cause layout issues.

Copilot uses AI. Check for mistakes.
sorter: true,
sortOrder: filter.sort.sortBy === "id" ? filter.sort.sortType : false,
onHeaderCell: (column) => ({
Expand Down Expand Up @@ -102,6 +103,7 @@ const generateBasicColumn = (
dataIndex: "action",
key: "action",
fixed: "right",
width: 200,
render: (_, record) => (
<Space size="middle">
<ButtonShow id={record.id} />
Expand Down
43 changes: 43 additions & 0 deletions src/components/Dashboard/ChartCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Card } from 'antd';
import PropTypes from 'prop-types';

/**
* ChartCard - Card wrapper cho charts với styling đẹp
*/
const ChartCard = ({
title,
children,
extra = null,
height = 400,
loading = false,
className = ''
}) => {
return (
<Card
title={title}
extra={extra}
loading={loading}
className={`shadow-sm hover:shadow-md transition-shadow duration-300 ${className}`}
bodyStyle={{
padding: '20px',
height: height - 70, // Subtract header height
overflow: 'hidden'
}}
>
<div className="w-full h-full">
{children}
</div>
</Card>
);
};

ChartCard.propTypes = {
title: PropTypes.string,
children: PropTypes.node.isRequired,
extra: PropTypes.node,
height: PropTypes.number,
loading: PropTypes.bool,
className: PropTypes.string,
};

export default ChartCard;
Loading
Loading