access logs + audio
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
import Quill from 'quill'
|
||||
import VideoBlot, { getEmbedUrl } from './VideoBlot'
|
||||
import 'quill/dist/quill.snow.css'
|
||||
|
||||
// ثبت VideoBlot در Quill
|
||||
Quill.register('formats/video', VideoBlot)
|
||||
|
||||
interface QuillEditorProps {
|
||||
value?: string
|
||||
onChange?: (content: string) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
minHeight?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
const QuillEditor: FC<QuillEditorProps> = ({
|
||||
value = '',
|
||||
onChange,
|
||||
placeholder = '',
|
||||
className = '',
|
||||
minHeight = '200px',
|
||||
id = 'quill-editor'
|
||||
}) => {
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
const quillRef = useRef<Quill | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return
|
||||
|
||||
// پیکربندی toolbar سفارشی
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'header': 1 }, { 'header': 2 }],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'direction': 'rtl' }],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'align': [] }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
|
||||
const quill = new Quill(editorRef.current, {
|
||||
theme: 'snow',
|
||||
placeholder,
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
}
|
||||
})
|
||||
|
||||
// تنظیم محتوای اولیه
|
||||
if (value) {
|
||||
quill.root.innerHTML = value
|
||||
}
|
||||
|
||||
// مدیریت تغییرات
|
||||
quill.on('text-change', () => {
|
||||
const html = quill.root.innerHTML
|
||||
onChange?.(html)
|
||||
})
|
||||
|
||||
quillRef.current = quill
|
||||
|
||||
return () => {
|
||||
quillRef.current = null
|
||||
}
|
||||
}, [placeholder])
|
||||
|
||||
// بروزرسانی محتوا از خارج
|
||||
useEffect(() => {
|
||||
if (quillRef.current && value !== quillRef.current.root.innerHTML) {
|
||||
quillRef.current.root.innerHTML = value
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const handleYouTubeClick = () => {
|
||||
const url = prompt('لینک ویدیو یوتیوب را وارد کنید:')
|
||||
if (url && quillRef.current) {
|
||||
const embedUrl = getEmbedUrl(url)
|
||||
if (embedUrl) {
|
||||
const range = quillRef.current.getSelection() || { index: 0, length: 0 }
|
||||
quillRef.current.insertEmbed(range.index, 'video', {
|
||||
url: embedUrl,
|
||||
width: '100%',
|
||||
height: '315'
|
||||
})
|
||||
quillRef.current.setSelection(range.index + 1)
|
||||
} else {
|
||||
alert('لینک ویدیو معتبر نیست')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAparatClick = () => {
|
||||
const url = prompt('لینک ویدیو آپارات را وارد کنید:')
|
||||
if (url && quillRef.current) {
|
||||
const embedUrl = getEmbedUrl(url)
|
||||
if (embedUrl) {
|
||||
const range = quillRef.current.getSelection() || { index: 0, length: 0 }
|
||||
quillRef.current.insertEmbed(range.index, 'video', {
|
||||
url: embedUrl,
|
||||
width: '100%',
|
||||
height: '315'
|
||||
})
|
||||
quillRef.current.setSelection(range.index + 1)
|
||||
} else {
|
||||
alert('لینک ویدیو معتبر نیست')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* دکمههای ویدیو بالای ادیتور */}
|
||||
<div className="flex gap-2 mb-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleYouTubeClick}
|
||||
className="px-3 py-1 text-xs font-bold text-red-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||
title="درج ویدیو یوتیوب"
|
||||
>
|
||||
📺 YouTube
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAparatClick}
|
||||
className="px-3 py-1 text-xs font-bold text-blue-600 border border-gray-300 rounded hover:bg-gray-50"
|
||||
title="درج ویدیو آپارات"
|
||||
>
|
||||
🎬 آپارات
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={editorRef}
|
||||
id={id}
|
||||
style={{ minHeight }}
|
||||
className="quill-editor-container"
|
||||
/>
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.ql-video-wrapper {
|
||||
margin: 1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
.ql-video-wrapper iframe {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuillEditor
|
||||
@@ -2,13 +2,40 @@ import { FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
color: string
|
||||
text?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
const StatusCircle: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'>
|
||||
const StatusCircle: FC<Props> = ({ color, text, size = 'md' }) => {
|
||||
const sizeClasses = {
|
||||
sm: 'size-2',
|
||||
md: 'size-3',
|
||||
lg: 'size-4'
|
||||
}
|
||||
|
||||
</div>
|
||||
const colorClasses = {
|
||||
green: 'bg-green-500',
|
||||
blue: 'bg-blue-500',
|
||||
red: 'bg-red-500',
|
||||
gray: 'bg-gray-500',
|
||||
purple: 'bg-purple-500',
|
||||
orange: 'bg-orange-500',
|
||||
yellow: 'bg-yellow-500'
|
||||
}
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`${sizeClasses[size]} ${colorClasses[color as keyof typeof colorClasses] || 'bg-gray-500'} rounded-full`} />
|
||||
<span className="text-sm font-medium">{text}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${sizeClasses[size]} rounded-full ${colorClasses[color as keyof typeof colorClasses] || 'bg-gray-500'}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ type Props = {
|
||||
preview?: string[],
|
||||
onChangePreview?: (preview: string[]) => void,
|
||||
getCover?: (url: string) => void,
|
||||
coverUrl?: string
|
||||
coverUrl?: string,
|
||||
accept?: string
|
||||
}
|
||||
|
||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
@@ -35,7 +36,10 @@ const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
accept: props.accept ? { [props.accept]: [] } : undefined
|
||||
})
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const array = [...files]
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import Quill from "quill";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const BlockEmbed = Quill.import("blots/block/embed") as any;
|
||||
|
||||
interface VideoData {
|
||||
url: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
class VideoBlot extends BlockEmbed {
|
||||
static blotName = "video";
|
||||
static tagName = "div";
|
||||
static className = "ql-video-wrapper";
|
||||
|
||||
static create(value: VideoData) {
|
||||
const node = super.create();
|
||||
node.classList.add("ql-video-wrapper");
|
||||
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("src", value.url);
|
||||
iframe.setAttribute("width", value.width || "100%");
|
||||
iframe.setAttribute("height", value.height || "315");
|
||||
iframe.setAttribute("frameborder", "0");
|
||||
iframe.setAttribute("allowfullscreen", "true");
|
||||
iframe.style.maxWidth = "100%";
|
||||
iframe.style.borderRadius = "8px";
|
||||
|
||||
node.appendChild(iframe);
|
||||
return node;
|
||||
}
|
||||
|
||||
static value(node: HTMLElement) {
|
||||
const iframe = node.querySelector("iframe");
|
||||
if (!iframe) return null;
|
||||
|
||||
return {
|
||||
url: iframe.getAttribute("src") || "",
|
||||
width: iframe.getAttribute("width") || "100%",
|
||||
height: iframe.getAttribute("height") || "315",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// تابع برای تشخیص و تبدیل URL های ویدیو
|
||||
export const getEmbedUrl = (url: string): string | null => {
|
||||
// YouTube
|
||||
const youtubeRegex = /(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/;
|
||||
const youtubeMatch = url.match(youtubeRegex);
|
||||
if (youtubeMatch) {
|
||||
return `https://www.youtube.com/embed/${youtubeMatch[1]}`;
|
||||
}
|
||||
|
||||
// Aparat
|
||||
const aparatRegex = /aparat\.com\/v\/([a-zA-Z0-9]+)/;
|
||||
const aparatMatch = url.match(aparatRegex);
|
||||
if (aparatMatch) {
|
||||
return `https://www.aparat.com/video/video/embed/videohash/${aparatMatch[1]}/vt/frame`;
|
||||
}
|
||||
|
||||
// اگر قبلاً embed URL است
|
||||
if (
|
||||
url.includes("youtube.com/embed/") ||
|
||||
url.includes("aparat.com/video/video/embed/")
|
||||
) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default VideoBlot;
|
||||
Reference in New Issue
Block a user