new order
This commit is contained in:
Generated
+39
@@ -17,6 +17,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
@@ -2509,6 +2510,15 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/axios": {
|
||||||
"version": "1.12.2",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
@@ -3198,6 +3208,18 @@
|
|||||||
"node": ">=16.0.0"
|
"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": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
@@ -4319,6 +4341,23 @@
|
|||||||
"react": "^19.2.0"
|
"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": {
|
"node_modules/react-element-popper": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"moment-jalaali": "^0.10.4",
|
"moment-jalaali": "^0.10.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-dropzone": "^14.3.8",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-loading-skeleton": "^3.5.0",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const Select: FC<Props> = (props: Props) => {
|
|||||||
}
|
}
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
<select {...props} className={clx(
|
<select {...props} className={clx(
|
||||||
'w-full bg-white input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors',
|
||||||
props.readOnly && 'bg-muted border-0 text-muted-foreground',
|
props.readOnly && 'bg-muted border-0 text-muted-foreground',
|
||||||
props.className,
|
props.className,
|
||||||
props.label && 'mt-1'
|
props.label && 'mt-1'
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { type FC, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useDropzone } from 'react-dropzone'
|
||||||
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
import { t } from '@/locale';
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
isMultiple?: boolean,
|
||||||
|
onChange?: (file: File[]) => void;
|
||||||
|
isReset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UploadBox: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<File[]>([])
|
||||||
|
|
||||||
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
|
if (props.isMultiple) {
|
||||||
|
const array = [...files]
|
||||||
|
array.push(acceptedFiles[0])
|
||||||
|
setFiles(array)
|
||||||
|
props.onChange?.(array)
|
||||||
|
} else {
|
||||||
|
setFiles([acceptedFiles[0]])
|
||||||
|
props.onChange?.([acceptedFiles[0]])
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [files])
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||||
|
|
||||||
|
const handleRemove = (index: number) => {
|
||||||
|
const array = [...files]
|
||||||
|
array.splice(index, 1)
|
||||||
|
setFiles(array)
|
||||||
|
props.onChange?.(array)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (props.isReset) {
|
||||||
|
setFiles([])
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [props.isReset])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='text-sm'>{props.label}</div>
|
||||||
|
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||||
|
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
|
{t('uploadBox.selectFile')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className='lg:flex gap-2 hidden'>
|
||||||
|
{
|
||||||
|
files?.map((item, index) => (
|
||||||
|
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||||
|
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||||
|
<div className='text-xs'>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
|
||||||
|
{
|
||||||
|
files?.map((item, index) => (
|
||||||
|
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||||
|
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||||
|
<div className='text-xs'>{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UploadBox
|
||||||
@@ -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<Props> = ({
|
||||||
|
label = 'پیام شما',
|
||||||
|
placeholder = 'پیام شما',
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onRecordingComplete
|
||||||
|
}) => {
|
||||||
|
const [isRecording, setIsRecording] = useState(false)
|
||||||
|
const [audioUrl, setAudioUrl] = useState<string | null>(null)
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
|
const [duration, setDuration] = useState(0)
|
||||||
|
|
||||||
|
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
|
const chunksRef = useRef<Blob[]>([])
|
||||||
|
const animationFrameRef = useRef<number | null>(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(
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-[2px] rounded-full transition-all duration-100"
|
||||||
|
style={{
|
||||||
|
height: `${randomHeight}px`,
|
||||||
|
backgroundColor: isPassed ? '#3B82F6' : '#E5E7EB'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bars
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{label && <div className="text-sm mb-1">{label}</div>}
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="w-full h-10 bg-white border border-border rounded-xl flex items-center px-4 gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange?.(e.target.value)}
|
||||||
|
className="flex-1 bg-transparent outline-none text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={isRecording ? stopRecording : startRecording}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
>
|
||||||
|
<Microphone
|
||||||
|
size={20}
|
||||||
|
color={isRecording ? '#EF4444' : '#000'}
|
||||||
|
variant={isRecording ? 'Bold' : 'Outline'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{audioUrl && (
|
||||||
|
<div className="w-full h-12 bg-white border border-border rounded-xl flex items-center px-3 gap-3 mt-3">
|
||||||
|
<button
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
className="w-8 h-8 rounded-full bg-black flex items-center justify-center flex-shrink-0"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<Pause size={16} color="#fff" variant="Bold" />
|
||||||
|
) : (
|
||||||
|
<Play size={16} color="#fff" variant="Bold" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex-1 flex items-center gap-[2px] h-8">
|
||||||
|
{generateWaveBars()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<audio ref={audioRef} src={audioUrl} className="hidden" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VoiceRecorder
|
||||||
@@ -43,6 +43,7 @@ textarea::placeholder {
|
|||||||
@theme {
|
@theme {
|
||||||
--color-primary: #ffa800;
|
--color-primary: #ffa800;
|
||||||
--color-border: #f5f7fc;
|
--color-border: #f5f7fc;
|
||||||
|
--color-secondary: #ebedf5;
|
||||||
--color-description: #c3c7dd;
|
--color-description: #c3c7dd;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,3 +70,7 @@ tbody tr:nth-child(odd) {
|
|||||||
.rmdp-container {
|
.rmdp-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rowTwoInput {
|
||||||
|
@apply flex flex-col items-start sm:flex-row sm:gap-5 gap-5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,4 +39,7 @@ export const fa = {
|
|||||||
contactUs: "با ما تماس بگیرید",
|
contactUs: "با ما تماس بگیرید",
|
||||||
phoneNumber: "۰۸۶۹۱۰۰۹۰۰۱",
|
phoneNumber: "۰۸۶۹۱۰۰۹۰۰۱",
|
||||||
},
|
},
|
||||||
|
uploadBox: {
|
||||||
|
selectFile: "انتخاب فایل",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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'
|
import { type FC } from 'react'
|
||||||
|
|
||||||
const NewOrder: FC = () => {
|
const NewOrder: FC = () => {
|
||||||
return (
|
return (
|
||||||
<div>NewOrder</div>
|
<div className='mt-5'>
|
||||||
|
<h1 className='text-lg font-light'>
|
||||||
|
سفارش جدید
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className='bg-white rounded-3xl p-6 mt-8'>
|
||||||
|
<div className='font-light'>اطلاعات سفارش</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Select
|
||||||
|
items={[]}
|
||||||
|
label='محصول'
|
||||||
|
placeholder='انتخاب محصول'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 rowTwoInput'>
|
||||||
|
<Select
|
||||||
|
items={[]}
|
||||||
|
label='تعداد'
|
||||||
|
placeholder='انتخاب'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label='انتخاب دلخواه'
|
||||||
|
placeholder='100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<VoiceRecorder
|
||||||
|
label='پیام صوتی'
|
||||||
|
onRecordingComplete={(blob) => {
|
||||||
|
console.log('ضبط صدا تکمیل شد:', blob)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<UploadBox
|
||||||
|
label='فایل های ضبط شده'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 flex justify-end'>
|
||||||
|
<Button
|
||||||
|
className='bg-transparent border border-primary text-primary w-fit px-6'
|
||||||
|
>
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
<AddSquare color={COLORS.primary} size={20} />
|
||||||
|
<div>
|
||||||
|
اضافه کردن
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user