diff --git a/package-lock.json b/package-lock.json index 174da11..0b3d63e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "moment-jalaali": "^0.10.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-dropzone": "^14.3.8", "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", @@ -2509,6 +2510,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", @@ -3198,6 +3208,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4319,6 +4341,23 @@ "react": "^19.2.0" } }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-element-popper": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz", diff --git a/package.json b/package.json index d0fd50a..d3ea02e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "moment-jalaali": "^0.10.4", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-dropzone": "^14.3.8", "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", diff --git a/src/components/Select.tsx b/src/components/Select.tsx index 6904d92..0dc8874 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -26,7 +26,7 @@ const Select: FC = (props: Props) => { }
+ + {t('uploadBox.selectFile')} + + +
+ { + files?.map((item, index) => ( +
+ handleRemove(index)} size={16} color='red' /> +
{item.name}
+
+ )) + } +
+
+
+ { + files?.map((item, index) => ( +
+ handleRemove(index)} size={16} color='red' /> +
{item.name}
+
+ )) + } +
+ + ) +} + +export default UploadBox \ No newline at end of file diff --git a/src/components/VoiceRecorder.tsx b/src/components/VoiceRecorder.tsx new file mode 100644 index 0000000..3aad16d --- /dev/null +++ b/src/components/VoiceRecorder.tsx @@ -0,0 +1,189 @@ +import { type FC, useState, useEffect, useRef } from 'react' +import { Microphone, Play, Pause } from 'iconsax-react' + +type Props = { + label?: string + placeholder?: string + value?: string + onChange?: (value: string) => void + onRecordingComplete?: (audioBlob: Blob) => void +} + +const VoiceRecorder: FC = ({ + label = 'پیام شما', + placeholder = 'پیام شما', + value, + onChange, + onRecordingComplete +}) => { + const [isRecording, setIsRecording] = useState(false) + const [audioUrl, setAudioUrl] = useState(null) + const [isPlaying, setIsPlaying] = useState(false) + const [currentTime, setCurrentTime] = useState(0) + const [duration, setDuration] = useState(0) + + const mediaRecorderRef = useRef(null) + const audioRef = useRef(null) + const chunksRef = useRef([]) + const animationFrameRef = useRef(null) + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + + const mediaRecorder = new MediaRecorder(stream) + mediaRecorderRef.current = mediaRecorder + chunksRef.current = [] + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunksRef.current.push(event.data) + } + } + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' }) + const url = URL.createObjectURL(audioBlob) + setAudioUrl(url) + onRecordingComplete?.(audioBlob) + stream.getTracks().forEach(track => track.stop()) + } + + mediaRecorder.start() + setIsRecording(true) + } catch (error) { + console.error('خطا در دسترسی به میکروفون:', error) + } + } + + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop() + setIsRecording(false) + } + } + + const togglePlayPause = () => { + if (!audioRef.current) return + + if (isPlaying) { + audioRef.current.pause() + } else { + audioRef.current.play() + } + setIsPlaying(!isPlaying) + } + + const updateProgress = () => { + if (audioRef.current) { + setCurrentTime(audioRef.current.currentTime) + if (isPlaying) { + animationFrameRef.current = requestAnimationFrame(updateProgress) + } + } + } + + useEffect(() => { + if (audioUrl && audioRef.current) { + audioRef.current.addEventListener('loadedmetadata', () => { + if (audioRef.current) { + setDuration(audioRef.current.duration) + } + }) + + audioRef.current.addEventListener('ended', () => { + setIsPlaying(false) + setCurrentTime(0) + }) + } + }, [audioUrl]) + + useEffect(() => { + if (isPlaying) { + updateProgress() + } + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPlaying]) + + const generateWaveBars = () => { + const bars = [] + const barCount = 100 + const progress = duration > 0 ? currentTime / duration : 0 + + for (let i = 0; i < barCount; i++) { + const position = i / barCount + const isPassed = position <= progress + const baseHeight = 3 + const randomHeight = Math.random() * 20 + baseHeight + + bars.push( +
+ ) + } + + return bars + } + + return ( +
+ {label &&
{label}
} + +
+
+ onChange?.(e.target.value)} + className="flex-1 bg-transparent outline-none text-sm" + /> + +
+ + {audioUrl && ( +
+ + +
+ {generateWaveBars()} +
+ +
+ )} +
+
+ ) +} + +export default VoiceRecorder diff --git a/src/index.css b/src/index.css index 20b1733..4288102 100644 --- a/src/index.css +++ b/src/index.css @@ -43,6 +43,7 @@ textarea::placeholder { @theme { --color-primary: #ffa800; --color-border: #f5f7fc; + --color-secondary: #ebedf5; --color-description: #c3c7dd; } @@ -69,3 +70,7 @@ tbody tr:nth-child(odd) { .rmdp-container { width: 100%; } + +.rowTwoInput { + @apply flex flex-col items-start sm:flex-row sm:gap-5 gap-5; +} diff --git a/src/locale/fa.ts b/src/locale/fa.ts index 37a45e5..b4a6511 100644 --- a/src/locale/fa.ts +++ b/src/locale/fa.ts @@ -39,4 +39,7 @@ export const fa = { contactUs: "با ما تماس بگیرید", phoneNumber: "۰۸۶۹۱۰۰۹۰۰۱", }, + uploadBox: { + selectFile: "انتخاب فایل", + }, }; diff --git a/src/pages/order/NewOrder.tsx b/src/pages/order/NewOrder.tsx index 1767083..cfeffbe 100644 --- a/src/pages/order/NewOrder.tsx +++ b/src/pages/order/NewOrder.tsx @@ -1,8 +1,72 @@ +import Button from '@/components/Button' +import Input from '@/components/Input' +import Select from '@/components/Select' +import UploadBox from '@/components/UploadBox' +import VoiceRecorder from '@/components/VoiceRecorder' +import { COLORS } from '@/constants/colors' +import { AddSquare } from 'iconsax-react' import { type FC } from 'react' const NewOrder: FC = () => { return ( -
NewOrder
+
+

+ سفارش جدید +

+ +
+
اطلاعات سفارش
+ +
+ + + +
+ +
+ { + console.log('ضبط صدا تکمیل شد:', blob) + }} + /> +
+ +
+ +
+ +
+ +
+
+
) }