access logs + audio

This commit is contained in:
hamid zarghami
2025-09-02 17:00:37 +03:30
parent 860f7c446f
commit 0e16af827f
25 changed files with 3394 additions and 83 deletions
+157
View File
@@ -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
+31 -4
View File
@@ -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'}`}
/>
)
}
+6 -2
View File
@@ -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]
+73
View File
@@ -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;