From 10245bdb088f04adbe22f010a1d3b87c2cd970d9 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 28 Jan 2026 16:01:00 +0330 Subject: [PATCH] order ticket --- .env | 2 +- src/components/VoicePlayer.tsx | 74 ++++++ src/config/func.ts | 21 ++ src/hooks/useVoiceRecorder.ts | 159 +++++++++++ src/pages/order/OrderDetail.tsx | 84 +----- src/pages/order/components/TicketSection.tsx | 261 +++++++++++++++++++ src/pages/order/hooks/useOrderData.ts | 21 ++ src/pages/order/service/OrderService.ts | 14 + src/pages/order/type/Types.ts | 21 ++ 9 files changed, 577 insertions(+), 80 deletions(-) create mode 100644 src/components/VoicePlayer.tsx create mode 100644 src/hooks/useVoiceRecorder.ts create mode 100644 src/pages/order/components/TicketSection.tsx diff --git a/.env b/.env index df8604e..52f1c95 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ VITE_TOKEN_NAME = 'negareh_t' VITE_REFRESH_TOKEN_NAME = 'negareh_rt' -VITE_API_BASE_URL = 'http://192.168.99.225:4000' +VITE_API_BASE_URL = 'http://10.24.161.1:4000' diff --git a/src/components/VoicePlayer.tsx b/src/components/VoicePlayer.tsx new file mode 100644 index 0000000..b96fab0 --- /dev/null +++ b/src/components/VoicePlayer.tsx @@ -0,0 +1,74 @@ +import { Pause, Play } from "iconsax-react" +import { useEffect, useRef, useState, type FC } from "react" + +type VoicePlayerProps = { + url: string +} + +const VoicePlayer: FC = ({ url }) => { + const audioRef = useRef(null) + const [isPlaying, setIsPlaying] = useState(false) + const [progress, setProgress] = useState(0) + + useEffect(() => { + const audio = audioRef.current + if (!audio) return + + const update = () => { + if (!audio.duration) return + setProgress(audio.currentTime / audio.duration) + } + + audio.addEventListener('timeupdate', update) + audio.addEventListener('ended', () => { + setIsPlaying(false) + setProgress(0) + }) + + return () => { + audio.removeEventListener('timeupdate', update) + } + }, []) + + const toggle = () => { + if (!audioRef.current) return + if (isPlaying) audioRef.current.pause() + else audioRef.current.play() + setIsPlaying(!isPlaying) + } + + return ( +
+ + +
+ {Array.from({ length: 60 }).map((_, i) => { + const passed = i / 60 <= progress + return ( +
+ ) + })} +
+ +
+ ) +} + +export default VoicePlayer \ No newline at end of file diff --git a/src/config/func.ts b/src/config/func.ts index d779275..780bcdf 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -65,3 +65,24 @@ export const extractErrorMessage = ( return fallbackMessage; } }; + +export const getFileNameAndExtensionFromUrl = ( + url: string, +): { fileName: string; extension: string } => { + try { + const cleanUrl = url.split("?")[0].split("#")[0]; + const lastPart = cleanUrl.substring(cleanUrl.lastIndexOf("/") + 1); + + if (!lastPart || !lastPart.includes(".")) { + return { fileName: "", extension: "" }; + } + + const lastDotIndex = lastPart.lastIndexOf("."); + const fileName = lastPart.substring(0, lastDotIndex); + const extension = lastPart.substring(lastDotIndex + 1); + + return { fileName, extension }; + } catch { + return { fileName: "", extension: "" }; + } +}; diff --git a/src/hooks/useVoiceRecorder.ts b/src/hooks/useVoiceRecorder.ts new file mode 100644 index 0000000..a991c7c --- /dev/null +++ b/src/hooks/useVoiceRecorder.ts @@ -0,0 +1,159 @@ +import { useState, useEffect, useRef, useCallback } from "react"; + +type UseVoiceRecorderOptions = { + onRecordingComplete?: (audioBlob: Blob) => void; +}; + +export const useVoiceRecorder = (options?: UseVoiceRecorderOptions) => { + const { onRecordingComplete } = options || {}; + + const [isRecording, setIsRecording] = useState(false); + const [audioUrl, setAudioUrl] = useState(null); + const [audioFile, setAudioFile] = 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 = useCallback(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 file = new File([audioBlob], "voice-message.webm", { + type: "audio/webm", + }); + const url = URL.createObjectURL(file); + + setAudioUrl(url); + setAudioFile(file); + onRecordingComplete?.(audioBlob); + + stream.getTracks().forEach((track) => track.stop()); + }; + + mediaRecorder.start(); + setIsRecording(true); + } catch (err) { + console.error("Microphone access error:", err); + } + }, [onRecordingComplete]); + + const stopRecording = useCallback(() => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + setIsRecording(false); + } + }, [isRecording]); + + const resetRecorder = useCallback(() => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + } + + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + } + + if (audioUrl) { + URL.revokeObjectURL(audioUrl); + } + + chunksRef.current = []; + setIsRecording(false); + setIsPlaying(false); + setCurrentTime(0); + setDuration(0); + setAudioUrl(null); + setAudioFile(null); + }, [audioUrl, isRecording]); + + const togglePlayPause = useCallback(() => { + if (!audioRef.current) return; + + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying((prev) => !prev); + }, [isPlaying]); + + const updateProgress = useCallback(() => { + if (!audioRef.current) return; + + setCurrentTime(audioRef.current.currentTime); + + if (isPlaying) { + animationFrameRef.current = requestAnimationFrame(updateProgress); + } + }, [isPlaying]); + + useEffect(() => { + if (!audioRef.current) return; + + const audio = audioRef.current; + + const onLoaded = () => setDuration(audio.duration); + const onEnded = () => { + setIsPlaying(false); + setCurrentTime(0); + }; + + audio.addEventListener("loadedmetadata", onLoaded); + audio.addEventListener("ended", onEnded); + + return () => { + audio.removeEventListener("loadedmetadata", onLoaded); + audio.removeEventListener("ended", onEnded); + }; + }, [audioUrl]); + + useEffect(() => { + if (isPlaying) updateProgress(); + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [isPlaying, updateProgress]); + + return { + // state + isRecording, + isPlaying, + audioUrl, + audioFile, + currentTime, + duration, + + // refs + audioRef, + + // actions + startRecording, + stopRecording, + togglePlayPause, + resetRecorder, + + // helpers + progress: duration ? currentTime / duration : 0, + }; +}; diff --git a/src/pages/order/OrderDetail.tsx b/src/pages/order/OrderDetail.tsx index d2a5bea..c00d188 100644 --- a/src/pages/order/OrderDetail.tsx +++ b/src/pages/order/OrderDetail.tsx @@ -1,17 +1,14 @@ -import { type FC, useState } from 'react' -import { Microphone, Paperclip2, Receipt1 } from 'iconsax-react' -import Button from '@/components/Button' -import UploadBox from '@/components/UploadBox' -import Input from '@/components/Input' +import { type FC } from 'react' +import { Receipt1 } from 'iconsax-react' import { useParams } from 'react-router-dom' import { useGetOrderDetails } from './hooks/useOrderData' +import TicketSection from './components/TicketSection' const OrderDetail: FC = () => { const { id } = useParams() const { data } = useGetOrderDetails(id!) - const [message, setMessage] = useState('') - const [files, setFiles] = useState([]) + const orderData = { orderId: '۱۲۲۴۵', @@ -22,9 +19,6 @@ const OrderDetail: FC = () => { description: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاق...' } - const handleSubmit = () => { - console.log('Submit:', { message, files }) - } return (
@@ -94,75 +88,7 @@ const OrderDetail: FC = () => {
{/* Bottom Section - White Card */} -
- {/* Designer Name */} -
- -
- -
-
- لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، -
- -
-
- -
loremipsum.pdf
-
-
-
- -
' -
-
-
طراح :
-
عباس حسینی
-
-
- لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است، چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است، و برای شرایط فعلی تکنولوژی مورد نیاز، و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد، کتابهای زیادی در شصت و سه درصد گذشته حال و آینده، شناخت فراوان جامعه و متخصصان را می طلبد، -
-
-
- - - -
-
پیام شما
-
-