update: fix packages
This commit is contained in:
+23
-25
@@ -1,21 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { Stage, OrbitControls, useGLTF } from '@react-three/drei'
|
||||
|
||||
function Model({ url }: { url: string }) {
|
||||
const { scene } = useGLTF(url)
|
||||
return <primitive object={scene} />
|
||||
}
|
||||
|
||||
function Loader() {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-6 h-6 border-2 border-gray-500 border-t-gray-300 rounded-full animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ModelViewerProps {
|
||||
url: string
|
||||
@@ -24,8 +9,28 @@ interface ModelViewerProps {
|
||||
}
|
||||
|
||||
export default function ModelViewer({ url, filename, size }: ModelViewerProps) {
|
||||
const [Scene, setScene] = useState<React.ComponentType<{ url: string }> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancel = false
|
||||
|
||||
import('./SceneViewer').then((mod) => {
|
||||
if (!cancel) setScene(() => mod.default)
|
||||
})
|
||||
|
||||
return () => { cancel = true }
|
||||
}, [])
|
||||
|
||||
if (!Scene) {
|
||||
return (
|
||||
<div className="w-full h-[450px] bg-black-800 border border-white/20 rounded-xl overflow-hidden flex items-center justify-center">
|
||||
<div className="w-6 h-6 border-2 border-gray-500 border-t-gray-300 rounded-full animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-[450px] bg-black-800 border border-black-700 rounded-xl overflow-hidden relative">
|
||||
<div className="w-full h-[450px] bg-black-800 border border-white/20 rounded-xl overflow-hidden relative">
|
||||
<div className="absolute top-3 left-3 z-10 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400 font-mono bg-black-900/60 px-2 py-1 rounded">
|
||||
{filename}
|
||||
@@ -34,14 +39,7 @@ export default function ModelViewer({ url, filename, size }: ModelViewerProps) {
|
||||
{size}
|
||||
</span>
|
||||
</div>
|
||||
<Canvas shadows dpr={[1, 2]} camera={{ fov: 50 }}>
|
||||
<Suspense fallback={null}>
|
||||
<Stage environment="city" intensity={0.6} adjustCamera={1.2}>
|
||||
<Model url={url} />
|
||||
</Stage>
|
||||
</Suspense>
|
||||
<OrbitControls makeDefault autoRotate autoRotateSpeed={0.5} />
|
||||
</Canvas>
|
||||
<Scene url={url} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense } from 'react'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { Stage, OrbitControls, useGLTF } from '@react-three/drei'
|
||||
|
||||
function Model({ url }: { url: string }) {
|
||||
const { scene } = useGLTF(url)
|
||||
return <primitive object={scene} />
|
||||
}
|
||||
|
||||
export default function SceneViewer({ url }: { url: string }) {
|
||||
return (
|
||||
<Canvas dpr={[1, 2]} camera={{ fov: 50 }}>
|
||||
<Suspense fallback={null}>
|
||||
<Stage environment="city" intensity={0.6} adjustCamera={1.2}>
|
||||
<Model url={url} />
|
||||
</Stage>
|
||||
</Suspense>
|
||||
<OrbitControls makeDefault autoRotate autoRotateSpeed={0.5} />
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
+73
-52
@@ -15,6 +15,7 @@ interface FileEntry {
|
||||
error?: string
|
||||
filename?: string
|
||||
previewUrl?: string
|
||||
viewerOpen?: boolean
|
||||
}
|
||||
|
||||
interface UploadResult {
|
||||
@@ -174,7 +175,7 @@ export default function UploadZone() {
|
||||
const newEntries: FileEntry[] = acceptedFiles
|
||||
.filter((f) => !existingNames.has(f.name))
|
||||
.map((file) => {
|
||||
const entry: FileEntry = { file, status: 'pending', progress: 0 }
|
||||
const entry: FileEntry = { file, status: 'pending', progress: 0, viewerOpen: true }
|
||||
const type = getFileType(file.name)
|
||||
if (type === 'model') {
|
||||
entry.previewUrl = URL.createObjectURL(file)
|
||||
@@ -190,7 +191,7 @@ export default function UploadZone() {
|
||||
accept: ACCEPTED_FORMATS,
|
||||
maxSize: 2 * 1024 * 1024 * 1024,
|
||||
disabled: isUploading,
|
||||
multiple: true,
|
||||
multiple: false,
|
||||
})
|
||||
|
||||
const allDone = files.length > 0 && files.every((f) => f.status === 'success')
|
||||
@@ -207,9 +208,9 @@ export default function UploadZone() {
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
placeholder="Enter secret key..."
|
||||
disabled={isUploading}
|
||||
className="w-full bg-black-800 border border-black-700 rounded-xl px-4 py-2.5 pr-12
|
||||
className="w-full bg-black-800 border border-white/30 rounded-xl px-4 py-2.5 pr-12
|
||||
text-gray-100 placeholder-gray-500 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-600 focus:border-gray-500
|
||||
focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-white/50
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition"
|
||||
/>
|
||||
<button
|
||||
@@ -243,52 +244,53 @@ export default function UploadZone() {
|
||||
onChange={(e) => setAssetName(e.target.value)}
|
||||
placeholder="e.g., tower, stone_floor, brick_wall..."
|
||||
disabled={isUploading}
|
||||
className="w-full bg-black-800 border border-black-700 rounded-xl px-4 py-2.5
|
||||
text-gray-100 placeholder-gray-500 text-sm font-mono
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-600 focus:border-gray-500
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition"
|
||||
className="w-full bg-black-800 border border-white/30 rounded-xl px-4 py-2.5
|
||||
text-gray-100 placeholder-gray-500 text-sm font-mono
|
||||
focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-white/50
|
||||
disabled:opacity-50 disabled:cursor-not-allowed transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`
|
||||
relative border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all duration-200 bg-black-800
|
||||
${isUploading ? 'cursor-not-allowed opacity-60 border-black-700' : ''}
|
||||
${isDragReject ? 'border-red-500 bg-red-900/20' : ''}
|
||||
${isDragActive && !isDragReject ? 'border-gray-400 bg-black-700 scale-[1.01]' : ''}
|
||||
${!isDragActive && !isDragReject && !isUploading
|
||||
? 'border-black-600 hover:border-gray-500 hover:bg-black-700'
|
||||
: ''}
|
||||
`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex justify-center mb-3">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center transition
|
||||
${isDragActive ? 'bg-gray-700' : 'bg-black-700'}`}>
|
||||
<svg className={`w-6 h-6 transition ${isDragActive ? 'text-white' : 'text-gray-400'}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
{files.length === 0 && (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`
|
||||
relative border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all duration-200 bg-black-800
|
||||
${isUploading ? 'cursor-not-allowed opacity-60 border-white/20' : ''}
|
||||
${isDragReject ? 'border-red-500 bg-red-900/20' : ''}
|
||||
${isDragActive && !isDragReject ? 'border-white/50 bg-black-700 scale-[1.01]' : ''}
|
||||
${!isDragActive && !isDragReject && !isUploading
|
||||
? 'border-white/30 hover:border-white/50 hover:bg-black-700'
|
||||
: ''}
|
||||
`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<div className="flex justify-center mb-3">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center transition ${isDragActive ? 'bg-gray-700' : 'bg-black-700'}`}>
|
||||
<svg className={`w-6 h-6 transition ${isDragActive ? 'text-white' : 'text-gray-400'}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{isDragReject ? (
|
||||
<p className="text-sm font-medium text-red-400">Unsupported format</p>
|
||||
) : isDragActive ? (
|
||||
<p className="text-sm font-medium text-gray-200">Release to add</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-gray-300">
|
||||
Drag files here
|
||||
<span className="text-gray-500 font-normal"> or click to browse</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Models: .glb, .gltf · Textures: .png, .jpg, .webp
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isDragReject ? (
|
||||
<p className="text-sm font-medium text-red-400">Unsupported format</p>
|
||||
) : isDragActive ? (
|
||||
<p className="text-sm font-medium text-gray-200">Release to add</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm font-medium text-gray-300">
|
||||
Drag files here
|
||||
<span className="text-gray-500 font-normal"> or click to browse</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Models: .glb, .gltf · Textures: .png, .jpg, .webp
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{globalError && (
|
||||
<p className="text-xs text-red-400 text-center">{globalError}</p>
|
||||
@@ -300,7 +302,7 @@ export default function UploadZone() {
|
||||
const type = getFileType(entry.file.name)
|
||||
return (
|
||||
<div key={`${entry.file.name}-${i}`}>
|
||||
<div className="flex items-center gap-3 bg-black-800 border border-black-700 rounded-xl px-4 py-3">
|
||||
<div className="flex items-center gap-3 bg-black-800 border border-white/20 rounded-xl px-4 py-3">
|
||||
|
||||
<div className="shrink-0">
|
||||
{entry.status === 'success' ? (
|
||||
@@ -323,11 +325,25 @@ export default function UploadZone() {
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-black-700 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (entry.previewUrl && type === 'model') {
|
||||
updateFile(i, { viewerOpen: !entry.viewerOpen })
|
||||
}
|
||||
}}
|
||||
className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition ${
|
||||
entry.previewUrl && type === 'model'
|
||||
? 'bg-black-700 hover:bg-gray-700 cursor-pointer'
|
||||
: 'bg-black-700 cursor-default'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-500 transition-transform ${entry.viewerOpen ? 'rotate-180' : ''}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -373,7 +389,11 @@ export default function UploadZone() {
|
||||
</div>
|
||||
|
||||
{entry.previewUrl && type === 'model' && entry.status !== 'success' && (
|
||||
<div className="mt-2">
|
||||
<div
|
||||
className={`transition-all duration-300 ease-in-out ${
|
||||
entry.viewerOpen ? 'max-h-[500px] opacity-100 mt-2' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<ModelViewer
|
||||
url={entry.previewUrl}
|
||||
filename={entry.file.name}
|
||||
@@ -392,8 +412,9 @@ export default function UploadZone() {
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
className="flex-1 bg-white text-black font-medium text-sm
|
||||
py-2.5 px-6 rounded-xl transition-opacity duration-150
|
||||
hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||
py-2.5 px-6 rounded-xl transition-all duration-150
|
||||
hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-white/50 border border-white/20"
|
||||
style={{ color: '#000000' }}
|
||||
>
|
||||
Upload {files.filter(f => f.status !== 'success').length > 1
|
||||
? `${files.filter(f => f.status !== 'success').length} files`
|
||||
|
||||
Reference in New Issue
Block a user