fix input shake on typing
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-06-26 16:32:40 +03:30
parent c06415536a
commit 2366f5ee02
5 changed files with 99 additions and 60 deletions
+22 -27
View File
@@ -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 bars = []
const barCount = 100 const barCount = 100
const progress = duration > 0 ? currentTime / duration : 0 const waveHeights = useMemo(
() => Array.from({ length: barCount }, () => Math.random() * 20 + 3),
for (let i = 0; i < barCount; i++) { [audioUrl]
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'
}}
/>
) )
} const progress = duration > 0 ? currentTime / duration : 0
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
+4
View File
@@ -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={(e) => handleChange(item.id, e.target.value)} onChange={handleChange}
/> />
</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>
) )
}) })
+18 -7
View File
@@ -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>
+1 -1
View File
@@ -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>