Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94c8df1328 | |||
| 615b4d2859 | |||
| a34b39ff5e | |||
| 9183f66f50 | |||
| 604104c875 | |||
| 54497158b0 | |||
| 698a3735a6 | |||
| 7ad2e91f60 | |||
| 0cbbc10d20 | |||
| 8ca0c54499 |
106
src/app/page.tsx
106
src/app/page.tsx
@@ -10,6 +10,7 @@ import TestimonialCardOne from '@/components/sections/testimonial/TestimonialCar
|
||||
import ContactFaq from '@/components/sections/contact/ContactFaq';
|
||||
import FooterMedia from '@/components/sections/footer/FooterMedia';
|
||||
import { Award, Sparkles, Users, Music } from "lucide-react";
|
||||
import BackgroundMusicPlayer from '@/components/BackgroundMusicPlayer';
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
@@ -25,6 +26,7 @@ export default function LandingPage() {
|
||||
secondaryButtonStyle="glass"
|
||||
headingFontWeight="normal"
|
||||
>
|
||||
<BackgroundMusicPlayer />
|
||||
<div id="nav" data-section="nav">
|
||||
<NavbarStyleCentered
|
||||
navItems={[
|
||||
@@ -42,8 +44,95 @@ export default function LandingPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes scaleHover {
|
||||
from { transform: scale(1); }
|
||||
to { transform: scale(1.02); }
|
||||
}
|
||||
@keyframes shadowDepth {
|
||||
from { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
|
||||
to { box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); }
|
||||
}
|
||||
@keyframes slideInLeft {
|
||||
from { transform: translateX(-20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes fadeGlow {
|
||||
from { filter: drop-shadow(0 0 0px rgba(168, 85, 247, 0)); }
|
||||
to { filter: drop-shadow(0 0 20px rgba(168, 85, 247, 0.6)); }
|
||||
}
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(20px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes glowPulse {
|
||||
from { box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); }
|
||||
to { box-shadow: 0 0 30px rgba(59, 130, 246, 0.8); }
|
||||
}
|
||||
@keyframes floatUp {
|
||||
from { transform: translateY(10px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
#hero-section {
|
||||
transition: all 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
#hero-section:hover {
|
||||
animation: scaleHover 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
#about-section {
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
#about-section:hover {
|
||||
animation: shadowDepth 0.6s ease-out forwards;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
#features-section {
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
#features-section:hover {
|
||||
animation: slideInLeft 0.5s ease-in-out forwards;
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
#team-section {
|
||||
transition: all 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
#team-section:hover {
|
||||
animation: fadeGlow 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
#testimonials-section {
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
#testimonials-section:hover {
|
||||
animation: slideInRight 0.6s ease-out forwards;
|
||||
box-shadow: 0 15px 35px rgba(168, 85, 247, 0.2);
|
||||
}
|
||||
|
||||
#contact-section {
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
#contact-section:hover {
|
||||
animation: glowPulse 0.5s ease-in-out forwards;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
footer {
|
||||
transition: all 0.6s ease-out;
|
||||
}
|
||||
footer:hover {
|
||||
animation: floatUp 0.6s ease-out forwards;
|
||||
box-shadow: 0 -10px 30px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div id="hero-section" data-section="hero-section">
|
||||
<HeroLogoBillboardSplit
|
||||
<HeroImageOverlay
|
||||
logoText="Harmony Studios"
|
||||
description="Where musical excellence meets cutting-edge technology. We craft sonic experiences that resonate with artists and audiences alike."
|
||||
background={{ variant: "radial-gradient" }}
|
||||
@@ -52,17 +141,16 @@ export default function LandingPage() {
|
||||
{ text: "Book a Session", href: "#contact-section" }
|
||||
]}
|
||||
buttonAnimation="slide-up"
|
||||
layoutOrder="default"
|
||||
imageSrc="https://img.b2bpic.net/free-photo/piano-key-guitar-string-musician-creativity-generated-by-ai_188544-25821.jpg"
|
||||
imageAlt="Elegant recording studio setup with professional audio equipment"
|
||||
mediaAnimation="blur-reveal"
|
||||
frameStyle="card"
|
||||
overlayOpacity={0.4}
|
||||
ariaLabel="Hero section featuring Harmony Studios branding and studio imagery"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="about-section" data-section="about-section">
|
||||
<MetricSplitMediaAbout
|
||||
<TimelineAbout
|
||||
title="Crafting Soundscapes That Define Artistry"
|
||||
description="With over two decades of experience, our studio has been the creative sanctuary where legendary tracks are born. We blend technical excellence with artistic vision to deliver recordings that resonate beyond the studio walls."
|
||||
metrics={[
|
||||
@@ -83,7 +171,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div id="features-section" data-section="features-section">
|
||||
<FeatureCardThree
|
||||
<FeatureCardTwo
|
||||
title="Professional Recording Services"
|
||||
description="State-of-the-art facilities and expert engineers to bring your musical vision to life with unparalleled sound quality."
|
||||
tag="Studio Excellence"
|
||||
@@ -116,7 +204,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div id="team-section" data-section="team-section">
|
||||
<TeamCardSix
|
||||
<TeamCardFour
|
||||
members={[
|
||||
{
|
||||
id: "member-1", name: "Alexandra Chen", role: "Lead Producer & Engineer", imageSrc: "https://img.b2bpic.net/free-photo/music-producer-using-his-headphones-mix-master-session_482257-121292.jpg", imageAlt: "Alexandra Chen, Lead Producer & Engineer at Melody Studio"
|
||||
@@ -155,7 +243,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div id="testimonials-section" data-section="testimonials-section">
|
||||
<TestimonialCardOne
|
||||
<TestimonialCardThree
|
||||
testimonials={[
|
||||
{
|
||||
id: "testimonial-1", name: "Alexandra Chen", role: "Lead Vocalist", company: "Midnight Echoes", rating: 5,
|
||||
@@ -190,7 +278,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div id="contact-section" data-section="contact-section">
|
||||
<ContactFaq
|
||||
<ContactForm
|
||||
faqs={[
|
||||
{
|
||||
id: "faq-1", title: "What recording packages do you offer?", content: "We provide comprehensive recording packages tailored to different needs, from basic tracking sessions to full production services including mixing, mastering, and artist development."
|
||||
@@ -220,7 +308,7 @@ export default function LandingPage() {
|
||||
</div>
|
||||
|
||||
<div id="footer-section" data-section="footer-section">
|
||||
<FooterMedia
|
||||
<FooterMinimal
|
||||
imageSrc="https://img.b2bpic.net/free-photo/artist-props-photography_23-2148885625.jpg"
|
||||
imageAlt="Elegant music studio interior with vintage instruments and warm lighting"
|
||||
columns={[
|
||||
|
||||
162
src/components/AudioPlayer.tsx
Normal file
162
src/components/AudioPlayer.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Play, Pause, Volume2, VolumeX } from 'lucide-react';
|
||||
|
||||
interface AudioPlayerProps {}
|
||||
|
||||
export function AudioPlayer({}: AudioPlayerProps) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [volume, setVolume] = useState(0.5);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [previousVolume, setPreviousVolume] = useState(0.5);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
const updateTime = () => setCurrentTime(audio.currentTime);
|
||||
const updateDuration = () => setDuration(audio.duration);
|
||||
const handleEnded = () => setIsPlaying(false);
|
||||
|
||||
audio.addEventListener('timeupdate', updateTime);
|
||||
audio.addEventListener('loadedmetadata', updateDuration);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', updateTime);
|
||||
audio.removeEventListener('loadedmetadata', updateDuration);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = isMuted ? 0 : volume;
|
||||
}
|
||||
}, [volume, isMuted]);
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (audioRef.current) {
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
if (isMuted) {
|
||||
setIsMuted(false);
|
||||
setVolume(previousVolume);
|
||||
} else {
|
||||
setPreviousVolume(volume);
|
||||
setIsMuted(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
setVolume(newVolume);
|
||||
if (isMuted && newVolume > 0) {
|
||||
setIsMuted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProgressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newTime = parseFloat(e.target.value);
|
||||
setCurrentTime(newTime);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = newTime;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
if (!time || isNaN(time)) return '0:00';
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-slate-900 to-slate-800 border-t border-slate-700 shadow-2xl z-50">
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
|
||||
<div className="max-w-full mx-auto px-4 py-4">
|
||||
{/* Progress Bar */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-xs text-slate-400 w-10 text-right">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={duration || 0}
|
||||
value={currentTime}
|
||||
onChange={handleProgressChange}
|
||||
className="flex-1 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
/>
|
||||
<span className="text-xs text-slate-400 w-10">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-600 hover:bg-blue-700 transition-colors text-white"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={20} fill="currentColor" />
|
||||
) : (
|
||||
<Play size={20} fill="currentColor" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-sm text-slate-300 font-medium">
|
||||
Background Music
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Volume Control */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full hover:bg-slate-700 transition-colors text-slate-300 hover:text-white"
|
||||
aria-label={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted ? <VolumeX size={18} /> : <Volume2 size={18} />}
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-24 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
aria-label="Volume"
|
||||
/>
|
||||
|
||||
<span className="text-xs text-slate-400 w-8 text-right">
|
||||
{Math.round((isMuted ? 0 : volume) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
161
src/components/BackgroundLofiMusicPlayer.tsx
Normal file
161
src/components/BackgroundLofiMusicPlayer.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Play, Pause, Volume2, VolumeX } from 'lucide-react';
|
||||
|
||||
interface BackgroundLofiMusicPlayerProps {}
|
||||
|
||||
export function BackgroundLofiMusicPlayer({}: BackgroundLofiMusicPlayerProps) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [volume, setVolume] = useState(0.3);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
const [hasUserInteracted, setHasUserInteracted] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const lofiStreamUrl =
|
||||
'https://www.chosic.com/wp-content/uploads/2021/07/free-lofi-hip-hop-beats-background-music-for-videos.mp3';
|
||||
|
||||
useEffect(() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = isMuted ? 0 : volume;
|
||||
}
|
||||
}, [volume, isMuted]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleUserInteraction = () => {
|
||||
setHasUserInteracted(true);
|
||||
document.removeEventListener('click', handleUserInteraction);
|
||||
document.removeEventListener('keydown', handleUserInteraction);
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleUserInteraction);
|
||||
document.addEventListener('keydown', handleUserInteraction);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleUserInteraction);
|
||||
document.removeEventListener('keydown', handleUserInteraction);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
audioRef.current.play().catch((error) => {
|
||||
console.error('Autoplay failed:', error);
|
||||
});
|
||||
setIsPlaying(true);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
setIsMuted(!isMuted);
|
||||
};
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setVolume(parseFloat(e.target.value));
|
||||
setIsMuted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={lofiStreamUrl}
|
||||
loop
|
||||
crossOrigin="anonymous"
|
||||
onEnded={() => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`bg-gradient-to-br from-slate-800 to-slate-900 rounded-full shadow-lg border border-slate-700 transition-all duration-300 ${
|
||||
isExpanded ? 'w-64 rounded-2xl' : 'w-16 h-16'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between p-3 h-16">
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 transition-all text-white shadow-md hover:shadow-lg"
|
||||
title={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={20} fill="currentColor" />
|
||||
) : (
|
||||
<Play size={20} fill="currentColor" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="flex-1 flex items-center gap-3 ml-3">
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="flex-shrink-0 text-slate-300 hover:text-white transition-colors"
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted ? <VolumeX size={18} /> : <Volume2 size={18} />}
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="flex-1 h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500"
|
||||
title="Volume"
|
||||
/>
|
||||
|
||||
<span className="text-xs text-slate-400 w-8 text-right">
|
||||
{Math.round((isMuted ? 0 : volume) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isExpanded && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="absolute inset-0 rounded-full"
|
||||
title="Expand player"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="absolute top-2 right-2 text-slate-400 hover:text-white transition-colors"
|
||||
title="Collapse player"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-center text-xs text-slate-400">
|
||||
<p>🎵 Lofi Music</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
src/components/BackgroundMusicPlayer.tsx
Normal file
142
src/components/BackgroundMusicPlayer.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Volume2, VolumeX, Play, Pause } from 'lucide-react';
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function BackgroundMusicPlayer({}: Props) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [volume, setVolume] = useState(0.5);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
audio.volume = volume;
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
const handleCanPlay = () => {
|
||||
setIsLoaded(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setError('Failed to load audio');
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const handleEnded = () => {
|
||||
audio.currentTime = 0;
|
||||
audio.play().catch(() => {
|
||||
setIsPlaying(false);
|
||||
});
|
||||
};
|
||||
|
||||
audio.addEventListener('canplay', handleCanPlay);
|
||||
audio.addEventListener('error', handleError);
|
||||
audio.addEventListener('ended', handleEnded);
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('canplay', handleCanPlay);
|
||||
audio.removeEventListener('error', handleError);
|
||||
audio.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const togglePlayPause = async () => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio) return;
|
||||
|
||||
try {
|
||||
if (isPlaying) {
|
||||
audio.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
await audio.play();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Autoplay policy prevented playback');
|
||||
setIsPlaying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolume = parseFloat(e.target.value);
|
||||
setVolume(newVolume);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 bg-gradient-to-br from-slate-900 to-slate-800 rounded-lg shadow-2xl p-4 w-80 border border-slate-700">
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src="https://www.freepik.com/audio/tune/static"
|
||||
loop
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-white">Background Music</h3>
|
||||
<div className="text-xs text-slate-400">
|
||||
{isLoaded ? 'Ready' : 'Loading...'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-xs text-red-400 bg-red-950 bg-opacity-50 rounded px-2 py-1">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
disabled={!isLoaded}
|
||||
className="flex-shrink-0 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 disabled:cursor-not-allowed text-white rounded-full p-2 transition-colors duration-200"
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause size={20} fill="currentColor" />
|
||||
) : (
|
||||
<Play size={20} fill="currentColor" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
{volume === 0 ? (
|
||||
<VolumeX size={18} className="text-slate-400 flex-shrink-0" />
|
||||
) : (
|
||||
<Volume2 size={18} className="text-slate-400 flex-shrink-0" />
|
||||
)}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
aria-label="Volume"
|
||||
/>
|
||||
<span className="text-xs text-slate-400 w-8 text-right">
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500 text-center">
|
||||
{isPlaying ? 'Now playing' : 'Paused'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user