128 lines
5.1 KiB
TypeScript
128 lines
5.1 KiB
TypeScript
import { type FC, useCallback, useMemo, useState } from "react";
|
||
import { useEditorStore, type ToolType } from "@/pages/editor/store/editorStore";
|
||
import { useSingleUpload } from "@/pages/uploader/hooks/useUploaderData";
|
||
import { CloseCircle, Music } from "iconsax-react";
|
||
import { useDropzone } from "react-dropzone";
|
||
import AudioUploadZone from "@/components/AudioUploadZone";
|
||
import Input from "@/components/Input";
|
||
|
||
const getAudioNameFromFile = (file: File) =>
|
||
file.name.replace(/\.[^/.]+$/, "");
|
||
|
||
export const AUDIO_ACCEPT = {
|
||
"audio/*": [".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac", ".webm"],
|
||
} as const;
|
||
|
||
const AudioInput: FC = () => {
|
||
const { objects, addObject, setSelectedObjectId, setTool, deleteObject, updateObject } =
|
||
useEditorStore();
|
||
const { mutateAsync: uploadFile } = useSingleUpload();
|
||
const [isUploading, setIsUploading] = useState(false);
|
||
const [audioName, setAudioName] = useState("");
|
||
|
||
const onDrop = useCallback(
|
||
async (acceptedFiles: File[]) => {
|
||
if (acceptedFiles.length === 0) return;
|
||
|
||
setIsUploading(true);
|
||
try {
|
||
for (const file of acceptedFiles) {
|
||
try {
|
||
const result = await uploadFile({ file });
|
||
const audioUrl = result?.data?.url;
|
||
if (!audioUrl) continue;
|
||
const audioId = `audio-${Date.now()}-${Math.random()}`;
|
||
const newAudio = {
|
||
id: audioId,
|
||
type: "audio" as ToolType,
|
||
x: 100,
|
||
y: 100,
|
||
width: 320,
|
||
height: 56,
|
||
audioUrl,
|
||
audioName: audioName.trim() || getAudioNameFromFile(file),
|
||
};
|
||
addObject(newAudio);
|
||
setSelectedObjectId(newAudio.id);
|
||
setTool("select");
|
||
} catch {
|
||
// TODO: show error toast
|
||
}
|
||
}
|
||
setAudioName("");
|
||
} finally {
|
||
setIsUploading(false);
|
||
}
|
||
},
|
||
[addObject, audioName, setSelectedObjectId, setTool, uploadFile]
|
||
);
|
||
|
||
const { getRootProps, getInputProps } = useDropzone({
|
||
onDrop,
|
||
accept: AUDIO_ACCEPT,
|
||
multiple: true,
|
||
disabled: isUploading,
|
||
});
|
||
|
||
const audioObjects = useMemo(() => {
|
||
return objects.filter((obj) => obj.type === "audio");
|
||
}, [objects]);
|
||
|
||
const handleDeleteAudio = (audioId: string) => {
|
||
deleteObject(audioId);
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="text-base font-bold">صدا</div>
|
||
|
||
<Input
|
||
label="نام فایل صوتی"
|
||
value={audioName}
|
||
onChange={(e) => setAudioName(e.target.value)}
|
||
placeholder="مثلاً موسیقی پسزمینه"
|
||
/>
|
||
|
||
<AudioUploadZone
|
||
getRootProps={getRootProps}
|
||
getInputProps={getInputProps}
|
||
isLoading={isUploading}
|
||
/>
|
||
|
||
{audioObjects.length > 0 && (
|
||
<div className="space-y-2">
|
||
<div className="text-xs font-bold">فایلهای صوتی آپلود شده</div>
|
||
<div className="flex flex-col gap-2">
|
||
{audioObjects.map((audioObject) => (
|
||
<div
|
||
key={audioObject.id}
|
||
className="relative flex items-center gap-2 rounded-lg border border-border bg-gray-50 px-3 py-2"
|
||
>
|
||
<Music size={20} color="#8C90A3" variant="Bold" />
|
||
<input
|
||
className="flex-1 h-7 rounded-lg border border-border bg-white px-2 text-xs text-black outline-none"
|
||
value={audioObject.audioName || ""}
|
||
onChange={(e) =>
|
||
updateObject(audioObject.id, {
|
||
audioName: e.target.value,
|
||
})
|
||
}
|
||
placeholder="نام فایل صوتی"
|
||
/>
|
||
<button
|
||
onClick={() => handleDeleteAudio(audioObject.id)}
|
||
className="absolute -right-1 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center hover:bg-gray-100 transition-colors"
|
||
>
|
||
<CloseCircle size={16} color="red" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AudioInput;
|