Import source: src/components/GenreExplorer/GenreExplorer
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,129 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import { Canvas, useFrame } from '@react-three/fiber'
|
||||||
|
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'
|
||||||
|
|
||||||
|
const genres = [
|
||||||
|
{ name: 'Ambient', count: '12.4K', icon: '◉' },
|
||||||
|
{ name: 'Electronic', count: '48.2K', icon: '◈' },
|
||||||
|
{ name: 'Cinematic', count: '9.1K', icon: '◎' },
|
||||||
|
{ name: 'Dark Wave', count: '7.8K', icon: '◆' },
|
||||||
|
{ name: 'Synthwave', count: '31.5K', icon: '◇' },
|
||||||
|
{ name: 'Lo-Fi', count: '22.9K', icon: '○' },
|
||||||
|
{ name: 'Jazz Fusion', count: '5.3K', icon: '◐' },
|
||||||
|
{ name: 'Neo-Soul', count: '18.7K', icon: '◑' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function RotatingRing() {
|
||||||
|
const ref = useRef()
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
ref.current.rotation.x = clock.getElapsedTime() * 0.12
|
||||||
|
ref.current.rotation.y = clock.getElapsedTime() * 0.18
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<group ref={ref}>
|
||||||
|
<mesh>
|
||||||
|
<torusGeometry args={[2.2, 0.015, 8, 120]} />
|
||||||
|
<meshBasicMaterial color="#c9a84c" transparent opacity={0.35} />
|
||||||
|
</mesh>
|
||||||
|
<mesh rotation={[Math.PI / 3, 0, Math.PI / 4]}>
|
||||||
|
<torusGeometry args={[2.2, 0.015, 8, 120]} />
|
||||||
|
<meshBasicMaterial color="#e8c547" transparent opacity={0.2} />
|
||||||
|
</mesh>
|
||||||
|
<mesh rotation={[-Math.PI / 4, Math.PI / 3, 0]}>
|
||||||
|
<torusGeometry args={[2.2, 0.015, 8, 120]} />
|
||||||
|
<meshBasicMaterial color="#8a6f2e" transparent opacity={0.25} />
|
||||||
|
</mesh>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[0.6, 32, 32]} />
|
||||||
|
<meshStandardMaterial color="#c9a84c" emissive="#c9a84c" emissiveIntensity={0.4} roughness={0.05} metalness={1} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function GenreCard({ genre, index }) {
|
||||||
|
const x = useMotionValue(0)
|
||||||
|
const y = useMotionValue(0)
|
||||||
|
const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [10, -10]), { stiffness: 250, damping: 25 })
|
||||||
|
const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-10, 10]), { stiffness: 250, damping: 25 })
|
||||||
|
const cardRef = useRef()
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
const rect = cardRef.current.getBoundingClientRect()
|
||||||
|
x.set((e.clientX - rect.left) / rect.width - 0.5)
|
||||||
|
y.set((e.clientY - rect.top) / rect.height - 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={cardRef}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseLeave={() => { x.set(0); y.set(0) }}
|
||||||
|
style={{ rotateX, rotateY, transformStyle: 'preserve-3d', perspective: 600 }}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.07 }}
|
||||||
|
whileHover={{ scale: 1.05, y: -4 }}
|
||||||
|
className="relative cursor-pointer group rounded-2xl p-5 flex flex-col justify-between"
|
||||||
|
style={{ background: '#101010', border: '1px solid #1e1e1e', height: '130px' }}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-400"
|
||||||
|
style={{ boxShadow: 'inset 0 0 0 1px rgba(201,168,76,0.25), 0 0 30px rgba(201,168,76,0.08)' }} />
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<span className="text-2xl" style={{ color: 'rgba(201,168,76,0.6)' }}>{genre.icon}</span>
|
||||||
|
<span className="text-xs text-gray-700 font-mono">{genre.count}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="font-display font-semibold text-white text-sm group-hover:gold-text transition-colors">{genre.name}</h3>
|
||||||
|
<p className="text-xs text-gray-700 mt-0.5">tracks</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GenreExplorer() {
|
||||||
|
return (
|
||||||
|
<section id="discover" className="py-28 px-6" style={{ background: '#0a0a0a' }}>
|
||||||
|
<div className="section-divider mb-0" />
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mt-14">
|
||||||
|
|
||||||
|
{/* Left: 3D orbit animation */}
|
||||||
|
<div className="order-2 lg:order-1" style={{ height: '380px' }}>
|
||||||
|
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} gl={{ alpha: true, antialias: true }} style={{ background: 'transparent' }}>
|
||||||
|
<ambientLight intensity={0.1} />
|
||||||
|
<pointLight position={[0, 0, 5]} intensity={3} color="#c9a84c" />
|
||||||
|
<pointLight position={[4, 3, 2]} intensity={1.5} color="#e8c547" />
|
||||||
|
<RotatingRing />
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: genre cards */}
|
||||||
|
<div className="order-1 lg:order-2">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="mb-10"
|
||||||
|
>
|
||||||
|
<p className="text-xs tracking-[0.25em] uppercase gold-text mb-3">Browse</p>
|
||||||
|
<h2 className="font-display font-bold text-4xl md:text-5xl text-white">
|
||||||
|
Explore by <span className="gold-text">Genre</span>
|
||||||
|
</h2>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{genres.map((genre, i) => (
|
||||||
|
<GenreCard key={genre.name} genre={genre} index={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user