Add audio playback features with AudioBubble and AudioPlayer components; implement starry background effect in VoidCanvas

This commit is contained in:
2025-05-28 23:08:23 +02:00
parent 15de65af9d
commit 449c8cf808
4 changed files with 242 additions and 13 deletions

View File

@@ -1,23 +1,57 @@
'use client';
"use client"
import { dateToUnix, useNostrEvents } from "nostr-react";
import { useRef } from "react";
import { useRef, useState } from "react"
import { dateToUnix, useNostrEvents } from "nostr-react"
import AudioBubble from "@/components/audio-bubble"
import VoidCanvas from "@/components/void-canvas"
import AudioPlayer from "@/components/audio-player"
export default function Home() {
const now = useRef(new Date()); // Make sure current time isn't re-rendered
const now = useRef(new Date()) // Make sure current time isn't re-rendered
const [selectedAudio, setSelectedAudio] = useState<string | null>(null)
const [playingEventId, setPlayingEventId] = useState<string | null>(null)
// Fetch audio events according to NIP-94
const { events } = useNostrEvents({
filter: {
since: dateToUnix(now.current), // all new events from now
kinds: [1],
// since: dateToUnix(now.current), // all new events from now
kinds: [1063], // NIP-94 audio events
// "#m": ["audio/mpeg", "audio/ogg", "audio/wav", "audio/webm"], // Audio MIME types
"#m": ["audio/mpeg", "audio/ogg", "audio/wav"], // Audio MIME types
},
});
})
const handleBubbleClick = (url: string, eventId: string) => {
setSelectedAudio(url)
setPlayingEventId(eventId)
}
const handleAudioEnd = () => {
setSelectedAudio(null)
setPlayingEventId(null)
}
return (
<>
{events.map((event) => (
<p key={event.id}>{event.pubkey} posted: {event.content}</p>
))}
</>
);
<div className="relative w-full h-screen bg-black overflow-hidden">
<VoidCanvas>
{events.map((event) => {
// Extract audio URL from NIP-94 event
const url = event.tags.find((tag) => tag[0] === "url")?.[1]
if (!url) return null
return (
<AudioBubble
key={event.id}
eventId={event.id}
url={url}
isPlaying={event.id === playingEventId}
onClick={handleBubbleClick}
/>
)
})}
</VoidCanvas>
{selectedAudio && <AudioPlayer url={selectedAudio} onEnded={handleAudioEnd} />}
</div>
)
}

View File

@@ -0,0 +1,86 @@
"use client"
import { useEffect, useRef, useState } from "react"
import { Music } from "lucide-react"
interface AudioBubbleProps {
eventId: string
url: string
isPlaying: boolean
onClick: (url: string, eventId: string) => void
}
export default function AudioBubble({ eventId, url, isPlaying, onClick }: AudioBubbleProps) {
const bubbleRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState({
x: Math.random() * 80 + 10, // 10-90% of screen width
y: Math.random() * 80 + 10, // 10-90% of screen height
})
const [velocity, setVelocity] = useState({
x: (Math.random() - 0.5) * 0.2, // Random direction
y: (Math.random() - 0.5) * 0.2, // Random direction
})
const [size] = useState(Math.random() * 40 + 60) // 60-100px
// Handle bubble movement
useEffect(() => {
const interval = setInterval(() => {
setPosition((prev) => {
// Calculate new position
let newX = prev.x + velocity.x
let newY = prev.y + velocity.y
// Bounce off edges
if (newX < 0 || newX > 100) {
setVelocity((prev) => ({ ...prev, x: -prev.x }))
newX = prev.x
}
if (newY < 0 || newY > 100) {
setVelocity((prev) => ({ ...prev, y: -prev.y }))
newY = prev.y
}
return { x: newX, y: newY }
})
}, 50)
return () => clearInterval(interval)
}, [velocity])
// Add slight random movement changes
useEffect(() => {
const interval = setInterval(() => {
setVelocity((prev) => ({
x: prev.x + (Math.random() - 0.5) * 0.05,
y: prev.y + (Math.random() - 0.5) * 0.05,
}))
}, 2000)
return () => clearInterval(interval)
}, [])
const handleClick = () => {
onClick(url, eventId)
}
return (
<div
ref={bubbleRef}
className={`absolute rounded-full flex items-center justify-center cursor-pointer transition-all duration-300 ${
isPlaying ? "bg-purple-500 shadow-lg shadow-purple-500/50" : "bg-blue-500/70 hover:bg-blue-400/80"
}`}
style={{
left: `${position.x}%`,
top: `${position.y}%`,
width: `${size}px`,
height: `${size}px`,
transform: "translate(-50%, -50%)",
zIndex: isPlaying ? 10 : 1,
}}
onClick={handleClick}
>
<Music className={`${isPlaying ? "animate-pulse text-white" : "text-white/80"}`} size={size * 0.4} />
</div>
)
}

View File

@@ -0,0 +1,36 @@
"use client"
import { useEffect, useRef } from "react"
interface AudioPlayerProps {
url: string
onEnded: () => void
}
export default function AudioPlayer({ url, onEnded }: AudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
useEffect(() => {
if (!audioRef.current) {
audioRef.current = new Audio(url)
audioRef.current.addEventListener("ended", onEnded)
audioRef.current.play().catch((error) => {
console.error("Error playing audio:", error)
})
} else {
audioRef.current.src = url
audioRef.current.play().catch((error) => {
console.error("Error playing audio:", error)
})
}
return () => {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.removeEventListener("ended", onEnded)
}
}
}, [url, onEnded])
return null // Audio player is invisible
}

View File

@@ -0,0 +1,73 @@
"use client"
import type React from "react"
import { useEffect, useRef, useState } from "react"
interface VoidCanvasProps {
children: React.ReactNode
}
export default function VoidCanvas({ children }: VoidCanvasProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
// Create starry background effect
const container = containerRef.current
if (!container) return
const createStar = () => {
const star = document.createElement("div")
star.className = "absolute rounded-full bg-white opacity-70"
// Random size between 1-3px
const size = Math.random() * 2 + 1
star.style.width = `${size}px`
star.style.height = `${size}px`
// Random position
star.style.left = `${Math.random() * 100}%`
star.style.top = `${Math.random() * 100}%`
// Random twinkle animation
star.style.animation = `twinkle ${Math.random() * 5 + 3}s infinite`
container.appendChild(star)
// Remove after some time to prevent too many stars
setTimeout(() => {
container.removeChild(star)
}, 8000)
}
// Create initial stars
for (let i = 0; i < 50; i++) {
createStar()
}
// Add new stars periodically
const interval = setInterval(() => {
createStar()
}, 300)
return () => {
clearInterval(interval)
}
}, [])
return (
<div ref={containerRef} className="absolute inset-0 bg-black overflow-hidden">
<style jsx global>{`
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.8; }
}
`}</style>
{mounted && children}
</div>
)
}