drop-zone.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. "use client";
  2. import { useState, useCallback, useEffect, useRef } from "react";
  3. import { Upload } from "lucide-react";
  4. import { useEditorStore } from "@/lib/store";
  5. import { useIsMobile } from "@/lib/use-mobile";
  6. export function DropZone() {
  7. const [isDragging, setIsDragging] = useState(false);
  8. const [isUploading, setIsUploading] = useState(false);
  9. const dragCounter = useRef(0);
  10. const fileInputRef = useRef<HTMLInputElement>(null);
  11. const createCustomObject = useEditorStore((s) => s.createCustomObject);
  12. const isPlaying = useEditorStore((s) => s.isPlaying);
  13. const isMobile = useIsMobile();
  14. const hasGlbFiles = useCallback((e: DragEvent) => {
  15. if (e.dataTransfer?.types.includes("Files")) return true;
  16. return false;
  17. }, []);
  18. const processFiles = useCallback(
  19. async (files: File[]) => {
  20. const glbFiles = files.filter(
  21. (f) =>
  22. f.name.endsWith(".glb") ||
  23. f.name.endsWith(".gltf") ||
  24. f.type === "model/gltf-binary" ||
  25. f.type === "model/gltf+json",
  26. );
  27. if (glbFiles.length === 0) return;
  28. setIsUploading(true);
  29. for (const file of glbFiles) {
  30. try {
  31. let modelUrl: string;
  32. try {
  33. const formData = new FormData();
  34. formData.append("file", file);
  35. formData.append("filename", file.name);
  36. const res = await fetch("/api/upload-model", {
  37. method: "POST",
  38. body: formData,
  39. });
  40. if (res.ok) {
  41. const data = await res.json();
  42. modelUrl = data.url;
  43. } else {
  44. modelUrl = URL.createObjectURL(file);
  45. }
  46. } catch {
  47. modelUrl = URL.createObjectURL(file);
  48. }
  49. const name = file.name.replace(/\.(glb|gltf)$/i, "");
  50. createCustomObject("model", {
  51. name: name.charAt(0).toUpperCase() + name.slice(1),
  52. modelUrl,
  53. position: [0, 0, 0],
  54. scale: [1, 1, 1],
  55. });
  56. } catch (err) {
  57. console.error("Failed to add model:", err);
  58. }
  59. }
  60. setIsUploading(false);
  61. },
  62. [createCustomObject],
  63. );
  64. useEffect(() => {
  65. if (isPlaying) return;
  66. const handleDragEnter = (e: DragEvent) => {
  67. e.preventDefault();
  68. e.stopPropagation();
  69. dragCounter.current++;
  70. if (hasGlbFiles(e)) {
  71. setIsDragging(true);
  72. }
  73. };
  74. const handleDragOver = (e: DragEvent) => {
  75. e.preventDefault();
  76. e.stopPropagation();
  77. if (e.dataTransfer) {
  78. e.dataTransfer.dropEffect = "copy";
  79. }
  80. };
  81. const handleDragLeave = (e: DragEvent) => {
  82. e.preventDefault();
  83. e.stopPropagation();
  84. dragCounter.current--;
  85. if (dragCounter.current <= 0) {
  86. dragCounter.current = 0;
  87. setIsDragging(false);
  88. }
  89. };
  90. const handleDrop = async (e: DragEvent) => {
  91. e.preventDefault();
  92. e.stopPropagation();
  93. dragCounter.current = 0;
  94. setIsDragging(false);
  95. if (!e.dataTransfer) return;
  96. await processFiles(Array.from(e.dataTransfer.files));
  97. };
  98. window.addEventListener("dragenter", handleDragEnter);
  99. window.addEventListener("dragover", handleDragOver);
  100. window.addEventListener("dragleave", handleDragLeave);
  101. window.addEventListener("drop", handleDrop);
  102. return () => {
  103. window.removeEventListener("dragenter", handleDragEnter);
  104. window.removeEventListener("dragover", handleDragOver);
  105. window.removeEventListener("dragleave", handleDragLeave);
  106. window.removeEventListener("drop", handleDrop);
  107. };
  108. }, [isPlaying, hasGlbFiles, processFiles]);
  109. const handleFileInputChange = async (
  110. e: React.ChangeEvent<HTMLInputElement>,
  111. ) => {
  112. if (e.target.files) {
  113. await processFiles(Array.from(e.target.files));
  114. }
  115. if (fileInputRef.current) {
  116. fileInputRef.current.value = "";
  117. }
  118. };
  119. if (isPlaying) return null;
  120. return (
  121. <>
  122. {isDragging && (
  123. <div className="absolute inset-0 bg-blue-500/10 border-2 border-dashed border-blue-500/50 rounded-lg flex items-center justify-center z-[6] pointer-events-none">
  124. <div className="bg-black/70 backdrop-blur-sm rounded-lg px-6 py-4 flex flex-col items-center gap-2">
  125. <Upload className="w-8 h-8 text-blue-400" />
  126. <span className="text-sm text-blue-300 font-medium">
  127. Drop .glb file to add model
  128. </span>
  129. </div>
  130. </div>
  131. )}
  132. {isUploading && (
  133. <div className="absolute inset-0 flex items-center justify-center pointer-events-none z-[6]">
  134. <div className="bg-black/70 backdrop-blur-sm rounded-lg px-6 py-4">
  135. <span className="text-sm text-white">Adding model...</span>
  136. </div>
  137. </div>
  138. )}
  139. {isMobile && (
  140. <>
  141. <input
  142. ref={fileInputRef}
  143. type="file"
  144. accept=".glb,.gltf"
  145. multiple
  146. onChange={handleFileInputChange}
  147. className="hidden"
  148. />
  149. <button
  150. onClick={() => fileInputRef.current?.click()}
  151. className="absolute bottom-4 left-[4.5rem] z-10 w-11 h-11 flex items-center justify-center rounded-full bg-white/10 active:bg-white/30 text-white transition-colors backdrop-blur-sm"
  152. title="Import .glb model"
  153. >
  154. <Upload size={18} />
  155. </button>
  156. </>
  157. )}
  158. </>
  159. );
  160. }