This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useState, useEffect, useRef } from 'react'
|
import { memo, type FC, useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { Microphone, Play, Pause } from 'iconsax-react'
|
import { Microphone, Play, Pause } from 'iconsax-react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -9,7 +9,7 @@ type Props = {
|
|||||||
onRecordingComplete?: (audioBlob: Blob) => void
|
onRecordingComplete?: (audioBlob: Blob) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const VoiceRecorder: FC<Props> = ({
|
const VoiceRecorder: FC<Props> = memo(({
|
||||||
label = 'پیام شما',
|
label = 'پیام شما',
|
||||||
placeholder = 'پیام شما',
|
placeholder = 'پیام شما',
|
||||||
value,
|
value,
|
||||||
@@ -110,31 +110,12 @@ const VoiceRecorder: FC<Props> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isPlaying])
|
}, [isPlaying])
|
||||||
|
|
||||||
const generateWaveBars = () => {
|
const barCount = 100
|
||||||
const bars = []
|
const waveHeights = useMemo(
|
||||||
const barCount = 100
|
() => Array.from({ length: barCount }, () => Math.random() * 20 + 3),
|
||||||
const progress = duration > 0 ? currentTime / duration : 0
|
[audioUrl]
|
||||||
|
)
|
||||||
for (let i = 0; i < barCount; i++) {
|
const progress = duration > 0 ? currentTime / duration : 0
|
||||||
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 (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -175,7 +156,19 @@ const VoiceRecorder: FC<Props> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex-1 flex items-center gap-[2px] h-8">
|
<div className="flex-1 flex items-center gap-[2px] h-8">
|
||||||
{generateWaveBars()}
|
{waveHeights.map((height, i) => {
|
||||||
|
const isPassed = i / barCount <= progress
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-[2px] rounded-full transition-colors duration-100"
|
||||||
|
style={{
|
||||||
|
height: `${height}px`,
|
||||||
|
backgroundColor: isPassed ? '#3B82F6' : '#E5E7EB',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio ref={audioRef} src={audioUrl} className="hidden" />
|
<audio ref={audioRef} src={audioUrl} className="hidden" />
|
||||||
@@ -184,6 +177,8 @@ const VoiceRecorder: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
VoiceRecorder.displayName = 'VoiceRecorder'
|
||||||
|
|
||||||
export default VoiceRecorder
|
export default VoiceRecorder
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ tbody tr:nth-child(odd) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollbar-stable {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
.rowTwoInput {
|
.rowTwoInput {
|
||||||
@apply flex flex-col items-start sm:flex-row sm:gap-5 gap-5;
|
@apply flex flex-col items-start sm:flex-row sm:gap-5 gap-5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC } from 'react'
|
import { memo, useCallback, useRef, type FC } from 'react'
|
||||||
import type { AttributeType, RequestType } from '../type/Types'
|
import type { AttributeType, RequestType } from '../type/Types'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { FieldTypeEnum } from '../enum/RequestEnum'
|
import { FieldTypeEnum } from '../enum/RequestEnum'
|
||||||
@@ -15,25 +15,57 @@ type Props = {
|
|||||||
formik: FormikProps<RequestType>
|
formik: FormikProps<RequestType>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttributeFieldProps = {
|
||||||
|
item: AttributeType
|
||||||
|
value: string
|
||||||
|
onChange: (attributeId: string, value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttributeTextField = memo<AttributeFieldProps>(({ item, value, onChange }) => (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Input
|
||||||
|
label={item.name}
|
||||||
|
type={item.type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(item.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
AttributeTextField.displayName = 'AttributeTextField'
|
||||||
|
|
||||||
|
const AttributeTextareaField = memo<AttributeFieldProps>(({ item, value, onChange }) => (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Textarea
|
||||||
|
label={item.name}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(item.id, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
AttributeTextareaField.displayName = 'AttributeTextareaField'
|
||||||
|
|
||||||
const ManageAttribute: FC<Props> = (props) => {
|
const ManageAttribute: FC<Props> = (props) => {
|
||||||
|
|
||||||
const { attributes, formik } = props
|
const { attributes, formik } = props
|
||||||
|
const formikRef = useRef(formik)
|
||||||
|
formikRef.current = formik
|
||||||
|
|
||||||
const getAttributeValue = (fieldId: string) => {
|
const getAttributeValue = useCallback((fieldId: string) => {
|
||||||
const attr = formik.values.attributes.find((o) => o.fieldId === fieldId)
|
const attr = formikRef.current.values.attributes.find((o) => o.fieldId === fieldId)
|
||||||
return attr?.value != null ? String(attr.value) : ''
|
return attr?.value != null ? String(attr.value) : ''
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
const handleChange = (attributeId: string, value: string) => {
|
const handleChange = useCallback((attributeId: string, value: string) => {
|
||||||
const next = [...formik.values.attributes]
|
const current = formikRef.current
|
||||||
|
const next = [...current.values.attributes]
|
||||||
const index = next.findIndex((o) => o.fieldId === attributeId)
|
const index = next.findIndex((o) => o.fieldId === attributeId)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
next[index] = { ...next[index], value }
|
next[index] = { ...next[index], value }
|
||||||
} else {
|
} else {
|
||||||
next.push({ fieldId: attributeId, value })
|
next.push({ fieldId: attributeId, value })
|
||||||
}
|
}
|
||||||
formik.setFieldValue('attributes', next)
|
current.setFieldValue('attributes', next)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,24 +147,21 @@ const ManageAttribute: FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
else if (item.type === FieldTypeEnum.number || item.type === FieldTypeEnum.text)
|
else if (item.type === FieldTypeEnum.number || item.type === FieldTypeEnum.text)
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className='mt-6'>
|
<AttributeTextField
|
||||||
<Input
|
key={item.id}
|
||||||
label={item.name}
|
item={item}
|
||||||
type={item.type}
|
value={getAttributeValue(item.id)}
|
||||||
value={getAttributeValue(item.id)}
|
onChange={handleChange}
|
||||||
onChange={(e) => handleChange(item.id, e.target.value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
else if (item.type === FieldTypeEnum.textarea)
|
else if (item.type === FieldTypeEnum.textarea)
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className='mt-6'>
|
<AttributeTextareaField
|
||||||
<Textarea
|
key={item.id}
|
||||||
label={item.name}
|
item={item}
|
||||||
value={getAttributeValue(item.id)}
|
value={getAttributeValue(item.id)}
|
||||||
onChange={(e) => handleChange(item.id, e.target.value)}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'
|
import { useCallback, useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import VoiceRecorder from '@/components/VoiceRecorder'
|
import VoiceRecorder from '@/components/VoiceRecorder'
|
||||||
@@ -32,7 +32,7 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
const isEditing = editIndex !== null
|
const isEditing = editIndex !== null
|
||||||
|
|
||||||
const { data } = useGetProducts()
|
const { data } = useGetProducts()
|
||||||
const { setItems, items } = useRequestStore()
|
const setItems = useRequestStore((state) => state.setItems)
|
||||||
const [productSelected, setProductSelected] = useState<ProductType>()
|
const [productSelected, setProductSelected] = useState<ProductType>()
|
||||||
const [voiceFile, setVoiceFile] = useState<File>()
|
const [voiceFile, setVoiceFile] = useState<File>()
|
||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([])
|
||||||
@@ -74,10 +74,12 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
const payload: RequestType = { ...values, attachments }
|
const payload: RequestType = { ...values, attachments }
|
||||||
|
|
||||||
if (isEditing && editIndex !== null) {
|
if (isEditing && editIndex !== null) {
|
||||||
|
const items = useRequestStore.getState().items
|
||||||
const updated = [...items]
|
const updated = [...items]
|
||||||
updated[editIndex] = payload
|
updated[editIndex] = payload
|
||||||
setItems(updated)
|
setItems(updated)
|
||||||
} else {
|
} else {
|
||||||
|
const items = useRequestStore.getState().items
|
||||||
setItems([...items, payload])
|
setItems([...items, payload])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,18 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
|
|
||||||
const isUploading = singleUpload.isPending || multiUpload.isPending
|
const isUploading = singleUpload.isPending || multiUpload.isPending
|
||||||
|
|
||||||
|
const handleDescriptionChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
formik.setFieldValue('description', value)
|
||||||
|
},
|
||||||
|
[formik.setFieldValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRecordingComplete = useCallback((blob: Blob) => {
|
||||||
|
const file = new File([blob], 'recording.wav', { type: blob.type })
|
||||||
|
setVoiceFile(file)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white rounded-3xl p-6'>
|
<div className='bg-white rounded-3xl p-6'>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
@@ -160,12 +174,9 @@ const Request: FC<Props> = ({ editIndex, initialItem, onSaved, onCancelEdit }) =
|
|||||||
<div className='mt-6'>
|
<div className='mt-6'>
|
||||||
<VoiceRecorder
|
<VoiceRecorder
|
||||||
value={formik.values.description ?? ''}
|
value={formik.values.description ?? ''}
|
||||||
onChange={(value) => formik.setFieldValue('description', value)}
|
onChange={handleDescriptionChange}
|
||||||
label='پیام صوتی'
|
label='پیام صوتی'
|
||||||
onRecordingComplete={(blob) => {
|
onRecordingComplete={handleRecordingComplete}
|
||||||
const file = new File([blob], 'recording.wav', { type: blob.type })
|
|
||||||
setVoiceFile(file)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const MainRouter: FC = () => {
|
|||||||
<Header />
|
<Header />
|
||||||
<SideBar />
|
<SideBar />
|
||||||
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
|
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
|
||||||
<div className={`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
|
<div className='overflow-auto w-full max-h-[calc(100vh-113px)] scrollbar-stable'>
|
||||||
<div className='xl:pb-20 pb-32'>
|
<div className='xl:pb-20 pb-32'>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
Reference in New Issue
Block a user